1 /* BionicMessage.net Java GroupDAV library V1.0
4 * Created on February 26, 2006, 1:11 PM
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
25 package net
.bionicmessage
.groupdav
;
29 import net
.bionicmessage
.extutils
.*;
31 import java
.util
.logging
.*;
32 import javax
.xml
.parsers
.*;
33 import org
.xml
.sax
.InputSource
;
34 import javax
.net
.SocketFactory
;
35 import javax
.net
.ssl
.SSLContext
;
36 import javax
.net
.ssl
.SSLSocketFactory
;
37 import javax
.net
.ssl
.TrustManager
;
38 import javax
.net
.ssl
.X509TrustManager
;
41 * A basic GroupDAV implementation
42 * @author <a href="http://bionicmessage.net">Mathew McBride</a>.
44 public class groupDAV
implements IDAVHandler
{
46 public static final int MODE_GROUPDAV
= 0;
47 public static final int MODE_CALDAV
= 1;
48 public static final int MODE_CARDDAV
= 2;
49 private String host
= "";
50 private int po
= 2000;
51 private String base64cache
= "";
52 private String sdir
= "";
53 private static Logger logger
=
54 Logger
.getLogger(("funambol"));
55 private static ConsoleHandler ch
= new ConsoleHandler();
56 DocumentBuilderFactory fdb
= DocumentBuilderFactory
.newInstance();
57 private SocketFactory ssf
= null;
58 private boolean ssl
= false;
59 private Socket sock
= null;
60 private int offset
= 0;
61 private StringBuffer sbuf
= null;
62 private Thread th
= null;
63 public static final String USER_AGENT
= "BionicMessage.net GroupDAV {1.0a;Java}";
64 private String tok
= "http://";
65 private String origurl
= "";
66 private URL serverURL
= null;
67 private CalDAVExtensions cdavex
= null;
68 private CardDAVExtensions cardex
= null;
69 private ConcurrentDownloader cd
= null; // SAXDAVHandler stuff
70 private SAXDAVHandler sx
;
71 private boolean propReady
= false;
72 private Exception propRetException
= null;
73 private List
<HTTPCookie
> cookieJar
= null;
76 /** For use in situations where something (i.e in the case of
77 * Funambol's authentication) has already given us a HTTP Basic Base64
78 * representation of user:pass
79 * @param url The host and port of the server to connect to, in the
80 * format of http://hostname:port/
81 * @param b64cache A String pre-encoded in Base 64 representing
82 * "Autorization: Basic pass:user"
83 * (Google HTTP Basic authentication for more information)
85 public groupDAV(String url
, String b64cache
) {
86 base64cache
= "Authorization: Basic " + b64cache
;
90 /** Constructor using a http://// url, and plain text passwords.
91 * @param url The host and port of the server to connect to, in the
92 * format of http://hostname:port/
93 * @param user The username of the user connecting to the server
94 * @param pass The password of the user connecting to the server
96 public groupDAV(String url
, String user
, String pass
) {
97 base64cache
= "Authorization: Basic " + Base64
.encodeBytes(new String(user
+ ":" + pass
).getBytes());
101 /** Set the logger to output to
102 * @param log The logger instance to use
104 public void setLogger(Logger log
) {
109 /** Initiate client */
110 public void init(String url
) {
113 serverURL
= new URL(url
);
114 if (serverURL
.getProtocol().contains("https")) {
117 host
= serverURL
.getHost();
118 po
= serverURL
.getPort();
119 if (po
== -1 && !ssl
) {
121 } else if (po
== -1 && ssl
) {
124 } catch (MalformedURLException ex
) {
125 Logger
.getLogger(groupDAV
.class.getName()).log(Level
.SEVERE
, null, ex
);
126 throw new IllegalArgumentException(ex
);
128 if (!origurl
.substring(origurl
.length() - 1).equals("/")) {
131 logger
.setLevel(Level
.ALL
);
132 logger
.addHandler(ch
);
133 ch
.setLevel(Level
.ALL
);
134 logger
.info("GroupDAV client init()");
136 ssf
= SSLSocketFactory
.getDefault();
137 } catch (Exception e
) {
140 cdavex
= new CalDAVExtensions(serverURL
, this, logger
);
141 cardex
= new CardDAVExtensions(serverURL
, this, logger
);
142 cookieJar
= new ArrayList
<HTTPCookie
>(2); // don't expect
145 public int getUseMode(String path
) throws Exception
{
146 if (cdavex
.doesSupportCalDAV(Common
.createURL(path
, serverURL
))) {
148 } else if (cardex
.doesSupportCardDAV(Common
.createURL(path
, serverURL
))) {
151 return MODE_GROUPDAV
;
154 /** Send after client initation to discover object stores on the server
155 * @throws Exception When the server fails to supply discovery data */
156 /* public boolean findStores() throws Exception {
157 byte[] pfind = buildPROPFIND("");
158 String output = sendNonKeepAliveRequest(pfind);
159 int clength = findContentLength(output);
161 logger.warning("No Content-Length in findStores");
163 String xmldata = output.substring(output.length() - clength - 1, output.length());
164 xmldata = xmldata.trim();
165 if (!xmldata.contains("<?xml")) {
166 throw new Exception("No <?xml field in xmldata, dying");
168 logger.finer("Split=" + xmldata);
169 generateEntityList(xmldata, storeFinderHandler);
172 /** Get a list of store URL's from the server */
173 /* public Hashtable getURLLocs() {
174 Hashtable locations = new Hashtable();
175 ArrayList list = storeFinderHandler.getObjectList();
176 Hashtable dtypes = storeFinderHandler.getObjectDtypes();
177 Hashtable gtypes = storeFinderHandler.getObjectGtypes();
178 for (int i = 0; i < list.size(); i++) {
179 String url = (String) list.get(0);
180 String davtype = (String) dtypes.get(url);
181 if (davtype.equals("collection")) {
182 String gtype = (String) gtypes.get(url);
184 locations.put(url, gtype);
190 /** List objects in a store
192 public List
listObjects(String url
) throws Exception
{
193 logger
.fine("Listing objects for: " + url
);
194 byte[] pfind
= buildPROPFIND(url
);
195 HTTPInputStream istream
= Common
.sendNonKeepAliveRequest(serverURL
, pfind
);
196 InputSource is
= new InputSource(istream
);
198 propRetException
= null;
199 sx
= new SAXDAVHandler(this, is
);
201 } while (!propReady
);
202 if (propRetException
!= null) {
203 throw propRetException
;
205 addToCookieJar(istream
.getCookies());
206 Map
<String
, String
> etags
= sx
.getEtagMap();
207 List
<String
> urls
= sx
.getHrefs();
208 List
<String
> cleaned
= new ArrayList(urls
.size());
209 List
<String
> types
= sx
.getTypes();
210 for (int i
= 0; i
< urls
.size(); i
++) {
211 String lurl
= (String
) urls
.get(i
);
212 String type
= (String
) types
.get(i
);
213 String etag
= (String
) etags
.get(lurl
);
214 if (type
!= null && type
.contains("collection") || etag
.length() < 1) {
215 /* Don't add this location to the list; it is a collection */
216 logger
.finer("Collection: "+lurl
);
218 // Make sure all URLs coming out of here are FULL!
219 URL fullURL
= Common
.createURL(lurl
, serverURL
);
220 cleaned
.add(fullURL
.toString());
221 logger
.finer("Object: "+fullURL
.toExternalForm());
227 /** Posts an object */
228 public GroupDAVObject
postObject(String subdir
, String uid
, String contents
) throws Exception
{
229 // Todo - convert to full URL
230 if (!subdir
.endsWith("/")) {
231 subdir
= subdir
.concat("/");
234 String type
= "text/calendar;charset=utf-8";
235 if (contents
.indexOf("begin:vcard") != -1 ||
236 contents
.indexOf("BEGIN:VCARD") != -1) {
237 type
= "text/x-vcard;charset=utf-8";
239 HashMap headers
= new HashMap();
240 headers
.put("Content-Type", type
);
241 headers
.put("If-None-Match", "*");
242 URL fullURL
= Common
.createURL(subdir
+ "new.ics", serverURL
);
243 //byte[] query = buildGroupDAVQuery("PUT", subdir + "new.ics", contents, headers);
244 byte[] query
= Common
.buildQuery("PUT", fullURL
, contents
.getBytes(), headers
,
246 String q
= new String(query
);
247 String t
= Common
.sendNonKeepAliveRequest(serverURL
, query
, this, logger
);
248 logger
.fine("We got" + t
);
249 String firstLine
= t
.split("\r\n")[0];
250 if (firstLine
.indexOf("405") != -1) {
251 throw new Exception("Post error: " + t
);
253 GroupDAVObject gobjP
= new GroupDAVObject(t
, GroupDAVObject
.OBJECT_PUT
);
254 addToCookieJar(gobjP
.getCookies());
255 /* Did the server give us a location? If not, assume the location is exactly
256 * what we gave it (this rarely happens anyway) */
257 if (gobjP
.getLocation() == null) {
258 gobjP
.setLocation(origurl
+ subdir
+ uid
);
259 } else if (gobjP
.getLocation() != null) {
260 URL url
= Common
.createURL(gobjP
.getLocation(), serverURL
);
261 gobjP
.setLocation(url
.toString());
263 // Did the server give us an eTag with the PUT request or will we have
264 // to fetch the object?
265 if (gobjP
.getEtag() == null) {
266 GroupDAVObject gobjG
= getObject(gobjP
.getLocation());
273 /** Modifies an object on the server.
274 * @param addr Path to object on server (i.e /groupdav/Calendar/event.ics)
275 * @param contents New object contents
276 * @param etag Previous etag of object
277 * @return A GroupDAVObject with returned server contents
278 * @throws java.lang.Exception Due to network errors
280 public GroupDAVObject
modifyObject(String addr
,
282 String etag
) throws Exception
{
283 String type
= "text/calendar";
284 if (contents
.indexOf("begin:vcard") != -1 ||
285 contents
.indexOf("BEGIN:VCARD") != -1) {
286 type
= "text/x-vcard";
288 // Drop the hostname since we specify it in Host:
289 //addr = addr.replace(origurl, "/");
290 URL objAddr
= new URL(serverURL
, addr
);
292 HashMap headers
= new HashMap();
293 headers
.put("Content-Type", type
);
294 headers
.put("If-Match", etag
);
295 byte[] query
= Common
.buildQuery("PUT", objAddr
, contents
.getBytes("UTF-8"), headers
, this);
296 String q
= new String(query
);
297 String resp
= Common
.sendNonKeepAliveRequest(serverURL
, query
, this, logger
);
298 logger
.finest("We got: " + resp
);
299 String firstLine
= resp
.split("\r\n")[0];
300 GroupDAVObject gobjP
= new GroupDAVObject(resp
, GroupDAVObject
.OBJECT_PUT
);
301 if (gobjP
.getStatus() > 400 && gobjP
.getStatus() != 403) { // let 403 errors slide
302 throw new Exception("Error modifying object: " + firstLine
);
303 } else if (gobjP
.getStatus() == 403) {
304 return null; // Forbidden errors should be dealt with by the user.
306 /* Did the server give us a location? If not, assume the location is exactly
308 if (gobjP
.getLocation() == null) {
309 gobjP
.setLocation(addr
);
310 } else if (gobjP
.getLocation() != null) {
311 URL url
= Common
.createURL(gobjP
.getLocation(), serverURL
);
312 gobjP
.setLocation(url
.toString());
314 // Did the server give us an eTag with the PUT request or will we have
315 // to fetch the object?
316 if (gobjP
.getEtag() == null) {
317 GroupDAVObject gobjG
= getObject(gobjP
.getLocation());
324 public GroupDAVObject
deleteObject(String addr
, String etag
) throws Exception
{
325 URL netURL
= new URL(serverURL
, addr
);
326 HashMap header
= new HashMap();
327 header
.put("If-Match", etag
);
328 byte[] query
= Common
.buildQuery("DELETE", netURL
, new byte[0], header
, this);
329 String resp
= Common
.sendNonKeepAliveRequest(serverURL
, query
, this, logger
);
330 logger
.finest("We got: " + resp
);
331 GroupDAVObject gbo
= new GroupDAVObject(resp
, GroupDAVObject
.OBJECT_KILLED
);
335 /** Execute a GET command and return a GroupDAVObject */
336 public GroupDAVObject
getObject(String url
) throws Exception
{
337 //url = url.replace(tok + host + ":" + po, "");
338 URL pathURL
= Common
.createURL(url
, serverURL
);
339 String header
[] = {base64cache
};
340 byte[] send
= Common
.buildQuery("GET", pathURL
, new byte[0], null, this);
341 String st
= new String(send
);
342 String t
= Common
.sendNonKeepAliveRequest(serverURL
, send
, this, logger
);
343 String firstLine
= t
.split("\r\n")[0];
344 GroupDAVObject obj
= new GroupDAVObject(t
, GroupDAVObject
.OBJECT_GET
);
345 if (obj
.getStatus() > 400) {
346 throw new Exception("HTTP error encountered: "+firstLine
);
348 obj
.setLocation(pathURL
.toExternalForm());
352 public Map
<String
, String
> getURLsByEventEnding(String path
, String startRange
, String endRange
) throws Exception
{
353 //return cdavex.getByTimeRange(Common.createURL(path, serverURL), "VEVENT", "DTSTART", startRange, endRange);
354 return getURLsByTimeRange(path
, "VEVENT", "DTEND", startRange
, endRange
);
357 public Map
<String
, String
> getURLsByTimeRange(String path
, String type
, String property
, String startRange
,
358 String endRange
) throws Exception
{
359 return cdavex
.getByTimeRange(Common
.createURL(path
, serverURL
), type
, property
, startRange
, endRange
);
362 /** Obtain all objects from a server store */
363 public List
<GroupDAVObject
> getAllObjectsInPath(String path
, int typeOfQuery
, int downloadParameter
)
365 List toDL
= this.listObjects(path
);
366 return getObjectsInBulk(path
, toDL
,typeOfQuery
,downloadParameter
);
369 /** Download objects in bulk.
370 * @param root The root dir to get all objects from (i.e /groupdav/Calendar/)
371 * @param paths A list of objects to download
372 * @param typeOfQuery Server type
373 * @param downloadParamater Paramater for download driver
375 public List
<GroupDAVObject
> getObjectsInBulk(String root
,
378 int downloadParameter
) throws Exception
{
379 URL path
= Common
.createURL(root
, serverURL
);
380 List
<GroupDAVObject
> downloaded
= null;
381 downloaded
= new ArrayList
<GroupDAVObject
>(paths
.size());
382 if (paths
.size() > downloadParameter
&& typeOfQuery
!= MODE_GROUPDAV
) {
383 int downloadedCounter
= 0;
384 while (downloadedCounter
!= paths
.size()) {
385 int start
= downloadedCounter
;
387 if ((paths
.size() - downloadedCounter
) > downloadParameter
) {
388 finish
= (downloadedCounter
+ downloadParameter
);
390 finish
= start
+ (paths
.size() - downloadedCounter
);
392 List
<String
> toDL
= paths
.subList((downloadedCounter
), finish
);
393 downloaded
= performBulkFetch(path
, toDL
, typeOfQuery
, downloadParameter
, downloaded
);
394 downloadedCounter
= (downloadedCounter
+ toDL
.size());
397 downloaded
= performBulkFetch(path
, paths
, typeOfQuery
, downloadParameter
, downloaded
);
402 protected List
<GroupDAVObject
> performBulkFetch(
407 List
<GroupDAVObject
> addTo
) throws Exception
{
409 /* for (String u : paths) {
410 cd.addURLToDownload(u);
412 return cd.downloadObjects(); */
413 if (typeOfQuery
== MODE_GROUPDAV
) {
414 cd
= new ConcurrentDownloader(this);
415 for (String u
: paths
) {
416 cd
.addURLToDownload(u
);
418 return cd
.downloadObjects(addTo
,itemsAtOnce
);
419 } else if (typeOfQuery
== MODE_CALDAV
&& paths
.size() > 0) {
420 return cdavex
.doMultiget(root
, paths
, addTo
);
421 } else if (typeOfQuery
== MODE_CARDDAV
&& paths
.size() > 0) {
422 return cardex
.doMultiget(root
, paths
, addTo
);
424 return new ArrayList
<GroupDAVObject
>();
427 /** Build a PROPFIND query to list the contents of a collection
429 * @param subdir The server path to query
430 * @return A Byte array containing the HTTP headers and DAV query.
432 public byte[] buildPROPFIND(String subdir
) throws Exception
{
433 URL fullDir
= new URL(serverURL
, subdir
);
434 String XMLSTRING
= "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
435 "<propfind xmlns=\"DAV:\"><allprop/><prop><getetag/></prop></propfind>";
436 HashMap headers
= new HashMap();
437 headers
.put("Cache-control", "no-cache");
438 headers
.put("Depth", "1");
439 headers
.put("Content-Type", "text/xml;charset=utf-8");
440 byte[] query
= Common
.buildQuery("PROPFIND", fullDir
, XMLSTRING
.getBytes(), headers
, this);
445 private int findContentLength(String data
) {
446 String
[] headers
= data
.split("\n");
447 for (int i
= 0; i
< headers
.length
; i
++) {
448 String curHeader
= headers
[i
];
449 String
[] line
= curHeader
.split(":");
450 if (line
[0].equalsIgnoreCase("Content-Length")) {
451 Integer it
= new Integer(line
[1].trim());
452 return it
.intValue();
458 /** Returns a list of HTTP/DAV etags. Object URL's are the key in the Map.
459 * If the DAV PROPFIND did not return etag properties in its result
460 * an attempt to get() a etag will return null as per Map implemementation rules
462 public Map
<String
, String
> getEtags() {
463 // return dirFinderHandler.getObjectEtags();
464 /* We have to convert all URLs here to full */
465 Map etags
= sx
.getEtagMap();
466 Hashtable
<String
, String
> newEtags
= new Hashtable(etags
.size());
467 Set urls
= etags
.keySet();
468 Iterator urlIt
= urls
.iterator();
469 while (urlIt
.hasNext()) {
471 String url
= (String
) urlIt
.next();
472 String etag
= (String
) etags
.get(url
);
473 URL fullURL
= Common
.createURL(url
, serverURL
);
474 newEtags
.put(fullURL
.toString(), etag
);
475 } catch (MalformedURLException ex
) {
476 Logger
.getLogger(groupDAV
.class.getName()).log(Level
.SEVERE
, null, ex
);
483 /** Runs a HTTP OPTIONS operating to authenticate the user and return
484 * certain user parameters
487 * @throws java.lang.Exception
489 public Map
<String
, String
> returnParamsForUser(URL path
) throws Exception
{
490 byte[] query
= Common
.buildQuery("OPTIONS", path
, new byte[0], null, this);
491 String response
= Common
.sendNonKeepAliveRequest(path
, query
, this, logger
);
492 HashMap returnedParameters
= new HashMap();
493 GroupDAVObject opt_response
= new GroupDAVObject(response
, GroupDAVObject
.OBJECT_GET
);
494 if (opt_response
.getStatus() == 200) {
495 return returnedParameters
;
497 System
.err
.println("Returned response: " + opt_response
.getStatus());
502 public boolean getReady() {
506 public void setReady(boolean ready
) {
510 public void setRetException(Exception e
) {
511 propRetException
= e
;
513 public List
<HTTPCookie
> getCookieJar() {
516 public void addToCookieJar(List
<HTTPCookie
> newCookies
) {
517 /* For some reason Collection.addAll doesn't always seem to work
518 * Do the work ourselves */
519 for(HTTPCookie c
: newCookies
) {
523 public String
getBase64cache() {