- Moved the dtstart,dtend fixing to icalUtilities so it can be called from updateFrom...
[jgroupdav.git] / src / main / java / net / bionicmessage / groupdav / groupDAV.java
blob6bcfeda0651cd11199416cf4c5b724df956c6344
1 /* BionicMessage.net Java GroupDAV library V1.0
2 * groupDAV.java
4 * Created on February 26, 2006, 1:11 PM
5 *
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
21 * IN THE SOFTWARE.
25 package net.bionicmessage.groupdav;
27 import java.io.*;
28 import java.net.*;
29 import net.bionicmessage.extutils.*;
30 import java.util.*;
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;
40 /**
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 * &quot;Autorization: Basic pass:user&quot;
83 * (Google HTTP Basic authentication for more information)
85 public groupDAV(String url, String b64cache) {
86 base64cache = "Authorization: Basic " + b64cache;
87 init(url);
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());
98 init(url);
101 /** Set the logger to output to
102 * @param log The logger instance to use
104 public void setLogger(Logger log) {
105 logger = log;
106 cdavex.setLog(log);
109 /** Initiate client */
110 public void init(String url) {
111 origurl = url;
112 try {
113 serverURL = new URL(url);
114 if (serverURL.getProtocol().contains("https")) {
115 ssl = true;
117 host = serverURL.getHost();
118 po = serverURL.getPort();
119 if (po == -1 && !ssl) {
120 po = 80;
121 } else if (po == -1 && ssl) {
122 po = 443;
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("/")) {
129 origurl = url + "/";
131 logger.setLevel(Level.ALL);
132 logger.addHandler(ch);
133 ch.setLevel(Level.ALL);
134 logger.info("GroupDAV client init()");
135 try {
136 ssf = SSLSocketFactory.getDefault();
137 } catch (Exception e) {
138 e.printStackTrace();
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))) {
147 return MODE_CALDAV;
148 } else if (cardex.doesSupportCardDAV(Common.createURL(path, serverURL))) {
149 return MODE_CARDDAV;
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);
160 if (clength == -1) {
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);
170 return false;
171 } */
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);
183 if (gtype != null) {
184 locations.put(url, gtype);
188 return locations;
189 } */
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);
197 propReady = false;
198 propRetException = null;
199 sx = new SAXDAVHandler(this, is);
200 do {
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);
217 } else {
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());
224 return cleaned;
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("/");
233 uid = uid.trim();
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,
245 this);
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());
267 return gobjG;
268 } else {
269 return gobjP;
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,
281 String contents,
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
307 * what we gave it */
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());
318 return gobjG;
319 } else {
320 return gobjP;
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);
332 return gbo;
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());
349 return obj;
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)
364 throws Exception {
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,
376 List<String> paths,
377 int typeOfQuery,
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;
386 int finish = 0;
387 if ((paths.size() - downloadedCounter) > downloadParameter) {
388 finish = (downloadedCounter + downloadParameter);
389 } else {
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());
396 } else {
397 downloaded = performBulkFetch(path, paths, typeOfQuery, downloadParameter, downloaded);
399 return downloaded;
402 protected List<GroupDAVObject> performBulkFetch(
403 URL root,
404 List<String> paths,
405 int typeOfQuery,
406 int itemsAtOnce,
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);
441 return query;
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();
455 return -1;
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()) {
470 try {
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);
480 return newEtags;
483 /** Runs a HTTP OPTIONS operating to authenticate the user and return
484 * certain user parameters
485 * @param path
486 * @return
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;
496 } else {
497 System.err.println("Returned response: " + opt_response.getStatus());
499 return null;
502 public boolean getReady() {
503 return propReady;
506 public void setReady(boolean ready) {
507 propReady = ready;
510 public void setRetException(Exception e) {
511 propRetException = e;
513 public List<HTTPCookie> getCookieJar() {
514 return cookieJar;
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) {
520 cookieJar.add(c);
523 public String getBase64cache() {
524 return base64cache;