Fixed OpenGroupware etag discovery problem
[jgroupdav.git] / src / net / bionicmessage / groupdav / groupDAV.java
blobeda5034d87a733baab4c822b7d0e4745539f57ab
1 /* BionicMessage.net Java GroupDAV library V1.0
2 * groupDAV.java
4 * Created on February 26, 2006, 1:11 PM
6 Copyright (c) 2006 Mathew McBride / "BionicMessage.net"
7 Permission is hereby granted, free of charge, to any person obtaining a copy of this
8 software and associated documentation files (the "Software"), to deal in the Software
9 without restriction, including without limitation the rights to use, copy, modify, merge,
10 publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
11 to whom the Software is furnished to do so, subject to the following conditions:
13 The above copyright notice and this permission notice shall be included in all copies or
14 substantial portions of the Software.
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
17 INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
18 PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
19 FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20 OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 DEALINGS IN THE SOFTWARE.
26 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 org.xml.sax.helpers.DefaultHandler;
35 import javax.net.SocketFactory;
36 import javax.net.ssl.SSLSocketFactory;
37 import org.xml.sax.Attributes;
38 import org.xml.sax.SAXException;
39 /**
40 * A basic GroupDAV implementation, tested against Citadel.
41 * The HTTP and WebDAV stack here has been written from scratch
42 * since Citadel does not implement the full WebDAV spec (try connecting
43 * with a normal WebDAV client) and as such won't work with Slide etc.
44 * ... and its small :)
45 * @author <a href="http://bionicmessage.net">Mathew McBride</a>.
47 public class groupDAV {
48 private String u = "";
49 private String p = "";
50 private String host = "";
51 private int po = 2000;
52 private String base64cache = "";
53 private String sdir = "";
54 private static Logger logger =
55 Logger.getLogger(("funambol"));
56 private static ConsoleHandler ch = new ConsoleHandler();
57 DocumentBuilderFactory fdb = DocumentBuilderFactory.newInstance();
58 /* At the moment we only support one set of folders each. */
59 private String vtodoloc = "";
60 private String vcalloc = "";
61 private ArrayList vcallocs = null;
62 private String vaddrloc = "";
63 private SocketFactory ssf = null;
64 private boolean ssl = false;
65 private Socket sock = null;
66 private int offset = 0;
67 private StringBuffer sbuf = null;
68 private Thread th = null;
69 private static final String USER_AGENT = "BionicMessage.net GroupDAV {0.9;Java}";
70 private String tok = "http://";
71 private String origurl = "";
72 private entityFinderHandler storeFinderHandler = null;
73 private entityFinderHandler dirFinderHandler = null;
74 /** For use in situations where something (i.e in the case of
75 * Funambol's authentication) has already given us a HTTP Basic Base64
76 * representation of user:pass
77 * @param url The host and port of the server to connect to, in the
78 * format of http://hostname:port/
79 * @param b64cache A String pre-encoded in Base 64 representing
80 * &quot;Autorization: Basic pass:user&quot;
81 * (Google HTTP Basic authentication for more information)
83 public groupDAV(String url, String b64cache) {
84 origurl = url;
85 if (!origurl.substring(origurl.length()-1).equals("/")) {
86 origurl = url+"/";
88 if (url.indexOf("https://") != -1) {
89 ssl = true;
90 tok = "https://";
92 url = url.replaceAll(tok,""); // goodbye http://
93 String hostname = url.split(":")[0];
94 String ports = url.split(":")[1]; // port and url
95 ports = ports.replace("/","");
96 int port = Integer.valueOf(ports).intValue();
97 host = hostname;
98 po = port;
99 base64cache = "Authorization: Basic " + b64cache;
100 init();
102 /** Constructor using a http://// url, and plain text passwords.
103 * @param url The host and port of the server to connect to, in the
104 * format of http://hostname:port/
105 * @param user The username of the user connecting to the server
106 * @param pass The password of the user connecting to the server
108 public groupDAV(String url,String user, String pass) {
109 origurl = url;
110 if (url.indexOf("https://") != -1) {
111 ssl = true;
112 tok = "https://";
114 url = url.replaceAll(tok,""); // goodbye http://
115 String hostname = url.split(":")[0];
116 String ports = url.split(":")[1]; // port and url
117 ports = ports.replace("/","");
118 int port = Integer.valueOf(ports).intValue();
119 host = hostname;
120 po = port;
121 u = user;
122 p = pass;
123 base64cache = "Authorization: Basic " + Base64.encodeBytes(new String(u+":"+p).getBytes());
124 init();
126 /** Set the logger to output to
127 * @param log The logger instance to use
129 public void setLogger(Logger log) {
130 logger = log;
132 /** Initiate client */
133 public void init() {
134 logger.setLevel(Level.ALL);
135 logger.addHandler(ch);
136 ch.setLevel(Level.ALL);
137 logger.info("GroupDAV client init()");
138 storeFinderHandler = new entityFinderHandler(logger);
139 dirFinderHandler = new entityFinderHandler(logger);
141 ssf = SSLSocketFactory.getDefault();
142 try {
143 sbuf = new StringBuffer();
144 // sock = createSocket();
145 //sock.setKeepAlive(true);
146 // setupReadThread();
147 // th.setDaemon(true);
148 // th.start();
150 } catch (Exception ex) {
151 ex.printStackTrace();
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;
172 public entityFinderHandler getDirFinderHandler() {
173 return dirFinderHandler;
175 public entityFinderHandler getStoreFinderHandler() {
176 return storeFinderHandler;
178 /** Get a list of store URL's from the server */
179 public Hashtable getURLLocs() {
180 Hashtable locations = new Hashtable();
181 ArrayList list = storeFinderHandler.getObjectList();
182 Hashtable dtypes = storeFinderHandler.getObjectDtypes();
183 Hashtable gtypes = storeFinderHandler.getObjectGtypes();
184 for (int i = 0; i < list.size(); i++) {
185 String url = (String)list.get(0);
186 String davtype = (String)dtypes.get(url);
187 if (davtype.equals("collection")) {
188 String gtype = (String)gtypes.get(url);
189 if (gtype != null) {
190 locations.put(url,gtype);
194 return locations;
196 /** List objects in a store
198 public List listObjects(String url) throws Exception {
199 byte[] pfind = buildPROPFIND(url);
200 String output = sendNonKeepAliveRequest(pfind);
201 int clength = findContentLength(output);
202 if (clength == -1) {
203 logger.warning("We don't have a Content Length field in listObjects");
205 String xmldata = output.toString().split("\r\n\r\n")[1];
206 generateEntityList(xmldata,dirFinderHandler);
207 ArrayList objectList = dirFinderHandler.getObjectList();
208 Hashtable types = dirFinderHandler.getObjectDtypes();
209 ArrayList cleaned = new ArrayList();
210 for (int i = 0; i < objectList.size(); i++) {
211 String lurl = (String)objectList.get(i);
212 String type = (String)types.get(lurl);
213 if (type != null && type.contains("collection")) {
214 /* Don't add this location to the list; it is a collection */
215 } else {
216 cleaned.add(lurl);
219 return cleaned;
221 /** Posts an object */
222 public GroupDAVObject postObject(String subdir, String uid, String contents) throws Exception {
223 uid = uid.trim();
224 String type = "text/calendar";
225 if (contents.indexOf("begin:vcard") != -1 ||
226 contents.indexOf("BEGIN:VCARD") != -1) {
227 type = "text/x-vcard";
229 String[] headers = new String[] {
230 "Content-Type: " + type +"; charset=utf-8",
231 "If-None-Match: *",
232 base64cache
234 byte[] query = buildGroupDAVQuery("PUT",subdir+"new.ics",contents,headers);
235 String q = new String(query);
236 String t = sendNonKeepAliveRequest(query);
237 logger.fine("We got" + t);
238 if (t.indexOf("405") != -1) {
239 throw new Exception("Post error: " + t);
241 GroupDAVObject gobjP = new GroupDAVObject(t,GroupDAVObject.OBJECT_PUT);
242 /* Did the server give us a location? If not, assume the location is exactly
243 * what we gave it */
244 if (gobjP.getLocation() == null) {
245 gobjP.setLocation(origurl+subdir+uid);
246 } else if (gobjP.getLocation() != null) {
247 // String strippedurl = gobjP.getLocation().replace(tok+host+":"+po,"");
248 // gobjP.setLocation(strippedurl);
250 // Did the server give us an eTag with the PUT request or will we have
251 // to fetch the object?
252 if (gobjP.getEtag() == null) {
253 GroupDAVObject gobjG = getObject(gobjP.getLocation());
254 return gobjG;
255 } else {
256 return gobjP;
259 public GroupDAVObject modifyObject(String addr,
260 String contents,
261 String etag) throws Exception {
262 String type = "text/calendar";
263 if (contents.indexOf("begin:vcard") != -1 ||
264 contents.indexOf("BEGIN:VCARD") != -1) {
265 type = "text/x-vcard";
267 // Drop the hostname since we specify it in Host:
268 addr = addr.replace(origurl, "/");
270 String[] header = new String[] {base64cache,
271 "Content-Type: "+type+"; charset=utf-8","If-Match:" + etag};
272 byte[] query = buildGroupDAVQuery("PUT", addr, contents,header);
273 String q = new String(query);
274 String resp = sendNonKeepAliveRequest(query);
275 logger.finest("We got: " + resp);
276 if (resp.indexOf("405") != -1) {
277 throw new Exception("Modify error:" + resp);
279 GroupDAVObject gobjP = new GroupDAVObject(resp,GroupDAVObject.OBJECT_PUT);
280 /* Did the server give us a location? If not, assume the location is exactly
281 * what we gave it */
282 if (gobjP.getLocation() == null) {
283 gobjP.setLocation(addr);
284 } else if (gobjP.getLocation() != null) {
285 String strippedurl = gobjP.getLocation().replace(tok+host+":"+po,"");
286 gobjP.setLocation(strippedurl);
288 // Did the server give us an eTag with the PUT request or will we have
289 // to fetch the object?
290 if (gobjP.getEtag() == null) {
291 GroupDAVObject gobjG = getObject(gobjP.getLocation());
292 return gobjG;
293 } else {
294 return gobjP;
297 public GroupDAVObject deleteObject(String addr, String etag) throws Exception {
298 addr = addr.replace(origurl, "/");
299 String[] header = new String[] {base64cache,"If-Match:"+etag};
300 byte[] query = buildGroupDAVQuery("DELETE",addr,null,header);
301 String resp = sendNonKeepAliveRequest(query);
302 logger.finest("We got: " + resp);
303 GroupDAVObject gbo = new GroupDAVObject(resp,GroupDAVObject.OBJECT_KILLED);
304 return gbo;
306 /** Execute a GET command and return a GroupDAVObject */
307 public GroupDAVObject getObject(String url) throws Exception {
308 url = url.replace(tok + host + ":" + po,"");
309 String header[] = {base64cache};
310 byte[] send = buildGroupDAVQuery("GET",url,null,header);
311 String st = new String(send);
312 String t = sendNonKeepAliveRequest(send);
313 GroupDAVObject obj = new GroupDAVObject(t,GroupDAVObject.OBJECT_GET);
314 return obj;
316 /** TODO: Do a GroupDAV compliant PROPFIND */
317 public byte[] buildPROPFIND(String subdir) {
318 String gdir = sdir + "/";
319 if (subdir.indexOf(sdir) > -1) {
320 gdir = "";
322 String XMLSTRING = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
323 "<propfind xmlns=\"DAV:\"><allprop/><prop><getetag/></prop></propfind>";
324 // String XMLSTRING = "";
325 String PROPFIND = "PROPFIND " + gdir + subdir + " HTTP/1.1\n" +
326 "Cache-control: no-cache\nPragma: no-cache\nAccept-Language: en\n" +
327 base64cache + "\nContent-Length: " +
328 XMLSTRING.getBytes().length + "\nHost: " + host + ":" + po + "\n" +
329 "Depth: 1\nContent-Type: text/xml;charset=utf-8\nAccept: text/*\n\n" +
330 XMLSTRING;
331 return PROPFIND.getBytes();
334 /* public byte[] buildPROPFINDList(String subdir) {
335 String gdir = sdir + "/";
336 if (subdir.indexOf(sdir) > -1) {
337 gdir = "";
339 String XMLSTRING = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
340 "<propfind xmlns=\"DAV:\">" +
341 "<prop xmlns=\"DAV:\"><allprop xmlns=\"DAV:\"/><getetag xmlns=\"DAV:\"/>" +
342 "</prop></propfind>";
343 String PROPFIND = "PROPFIND " + gdir + subdir + " HTTP/1.1\n" +
344 "Cache-control: no-cache\nPragma: no-cache\nAccept-Language: en\n" +
345 base64cache + "\nContent-Length: " +
346 XMLSTRING.getBytes().length + "\nHost: " + host + ":" + po + "\n" +
347 "Content-Type: text/xml;charset=utf-8\nAccept: text/*\n\n" +
348 XMLSTRING;
349 return PROPFIND.getBytes();
351 } */
352 private void generateEntityList(String xmldata, entityFinderHandler efh) throws Exception {
353 org.apache.xerces.parsers.SAXParser sparser = new org.apache.xerces.parsers.SAXParser();
354 sparser.setFeature("http://xml.org/sax/features/namespaces",true);
355 sparser.setContentHandler(efh);
356 sparser.parse(new InputSource(
357 new StringReader(xmldata)));
359 /** Testing method. Edit source and replace with own
360 * server settings to test client. */
361 public static void main(String args[]) {
362 /* Todo: use GUI to ask for info */
363 String SERVER = "http://your.server.here:80";
364 groupDAV client = new groupDAV(SERVER,
366 "");
367 try {
368 List calobjects = client.listObjects("/groupdav/Calendar/");
369 Map etags = client.getEtags();
370 for (int i=0; i<calobjects.size(); i++) {
371 String objectu = (String)calobjects.get(i);
372 String eTag = null;
373 if (etags.get(objectu) != null) {
374 eTag = (String)etags.get(objectu); } else {
375 eTag = "not found";
377 GroupDAVObject gobj = client.getObject(objectu);
378 System.err.println(gobj.getContent() + "\n|etag-server="+eTag);
379 System.err.println("|etag-object="+gobj.getEtag());
380 String object = gobj.getContent();
383 java.util.Random rd = new java.util.Random();
384 int uid = rd.nextInt(32);
385 String u = ""+uid;
386 String object ="BEGIN:VCALENDAR\n" +
387 "PRODID:-//BM GroupDAV Client//Test Creation Object/EN\n" +
388 "VERSION:2.0\n" +
389 "METHOD:REQUEST\n" +
390 "BEGIN:VEVENT\n" +
391 "SUMMARY:Test object\n" +
392 "LOCATION:In dev slash null\n" +
393 "DESCRIPTION:Please don't fail me! I'm innocent!\n" +
394 "DTSTART:20060408T100059Z\n" +
395 "DTEND:20060408T110059Z\n" +
396 "TRANSP:OPAQUE\n" +
397 "UID:" + u + "\n" +
398 "SEQUENCE:1\n" +
399 "ORGANIZER:MAILTO:matt@comalies\n" +
400 "END:VEVENT\n" +
401 "END:VCALENDAR";
403 // GroupDAVObject gbo = client.postObject(calendarUrl,u,object);
404 //System.out.println("Etag for new object= " + gbo.getEtag());
405 // Test change operation
406 // object.replaceAll("don't","never");
407 // GroupDAVObject gboc = client.modifyObject(gbo.getLocation(),object,gbo.getEtag());
408 // GroupDAVObject gbod = client.deleteObject(gbo.getLocation(),gbo.getEtag());
409 } catch (Exception ex) {
411 ex.printStackTrace();
414 /** Parses the result of PROPFIND */
415 private class entityFinderHandler extends DefaultHandler {
416 Integer element_index;
417 Stack parents = null;
418 Hashtable charBuffers = null;
419 Hashtable element_names = null;
421 Logger lg;
422 String obj_href = ""; // DAV Object href
423 String obj_dtype = ""; // DAV resource type
424 String obj_gtype = ""; // GroupDAV resource type
425 String obj_status = ""; // DAV object status
426 String obj_etag = ""; // DAV Object etag
428 Hashtable object_dtypes = null;
429 Hashtable object_gtypes = null;
430 Hashtable object_etags = null;
431 Hashtable object_statuses = null;
432 ArrayList object_list = null;
433 /** Creates a new instance of entityFinderHandler */
434 public entityFinderHandler(Logger l ) {
435 lg = l;
437 public void startDocument() throws SAXException {
438 charBuffers = new Hashtable();
439 element_names = new Hashtable();
440 element_index = -1;
441 parents = new Stack();
442 parents.push( new Integer( element_index ) );
443 object_dtypes = new Hashtable();
444 object_gtypes = new Hashtable();
445 object_etags = new Hashtable();
446 object_statuses = new Hashtable();
447 object_list = new ArrayList();
450 public void endDocument() throws SAXException {
454 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
455 ++ element_index;
456 int parentNodeIx = ( (Integer)parents.peek() ).intValue();
457 // this element in turn becomes the parent for subsequent routines
458 Integer key = new Integer(element_index);
459 element_names.put(key,qName);
460 parents.push( key );
461 charBuffers.put(key,new StringBuffer());
462 // etc ...
465 public void endElement(String uri, String localName, String qName) throws SAXException {
466 Integer key = (Integer)parents.pop();
467 StringBuffer charBuffer = (StringBuffer)charBuffers.get(key);
468 if (uri.contains("groupdav.org")) { // GroupDAV namespace
469 if (localName.contains("vevent-collection") ||
470 localName.contains("vtodo-collection") ||
471 localName.contains("vcard-collection")) {
472 obj_gtype = localName;
474 } else if (uri.contains("DAV:")) { // DAV namespace
475 if (localName.contains("collection")) {
476 obj_dtype = localName;
477 } else if (localName.contains("status")) {
478 obj_status = charBuffer.toString();
479 } else if (localName.contains("href")) {
480 obj_href = charBuffer.toString();
481 } else if (localName.contains("getetag")) {
482 obj_etag = charBuffer.toString();
485 if (uri.contains("DAV:") && localName.contains("response")) {
486 // Stuff all the known data about the object into the collections
487 object_list.add(obj_href);
488 object_etags.put(obj_href,obj_etag);
489 object_statuses.put(obj_href,obj_status);
490 object_dtypes.put(obj_href,obj_dtype);
491 object_gtypes.put(obj_href,obj_gtype);
493 /* Reset ready for the next one */
494 if (localName.contains("response")) {
495 obj_href = "";
496 obj_dtype = "";
497 obj_gtype = "";
498 obj_status = "";
499 obj_etag = "";
503 public void characters(char[] ch, int start, int length) throws SAXException {
504 String chars = new String(ch,start,length);
505 if (chars.trim().length() > 0) {
506 Integer parent = (Integer)parents.peek();
507 StringBuffer charBuffer = (StringBuffer)charBuffers.get(parent);
508 charBuffer.append(chars);
511 public ArrayList getObjectList() {
512 return object_list;
514 public Hashtable getObjectEtags() {
515 return object_etags;
517 public Hashtable getObjectStatuses() {
518 return object_statuses;
520 public Hashtable getObjectDtypes() {
521 return object_dtypes;
523 public Hashtable getObjectGtypes() {
524 return object_gtypes;
527 /** Create a socket to the host */
528 private Socket createSocket() throws Exception {
529 Socket st = null;
530 if (ssl) {
531 st = ssf.createSocket(host,po);
532 } else {
533 st = new Socket(host,po);
535 return st;
538 private String readChars() throws Exception {
539 th.sleep(sock.getSoTimeout());
540 String chars = sbuf.toString();
541 // clear buffer
542 sbuf = new StringBuffer();
543 return chars;
545 private int findContentLength(String data) {
546 String[] headers = data.split("\n");
547 for (int i = 0; i < headers.length; i++) {
548 String curHeader = headers[i];
549 String[] line = curHeader.split(":");
550 if (line[0].equalsIgnoreCase("Content-Length")) {
551 Integer it = new Integer(line[1].trim());
552 return it.intValue();
555 return -1;
557 private String sendNonKeepAliveRequest(byte[] by) throws Exception{
558 String dbugstring = new String(by);
559 logger.finer("We sent:\r\n"+dbugstring);
560 // Measure latency
561 long time = new java.util.Date().getTime();
562 Socket st = createSocket();
563 st.getOutputStream().write(by);
564 st.getOutputStream().flush();
565 InputStream sc = st.getInputStream();
566 StringBuffer sb = new StringBuffer();
567 BufferedReader rb = new BufferedReader(new InputStreamReader(sc));
568 int i = 0;
569 char[] cbuf = new char[300];
570 while((i = rb.read(cbuf)) != -1) {
571 sb.append(cbuf,0,i);
573 st.close();
574 long closetime = new java.util.Date().getTime();
575 long rtt = closetime - time;
576 logger.finer("We got:\r\n"+sb.toString()+"\r\n....in " + rtt + "ms");
577 return sb.toString();
579 /** Returns a list of HTTP/DAV etags. Object URL's are the key in the Map.
580 * If the DAV PROPFIND did not return etag properties in its result
581 * an attempt to get() a etag will return null as per Map implemementation rules
583 public Map getEtags() {
584 return dirFinderHandler.getObjectEtags();
586 private byte[] buildGroupDAVQuery(String method, String addr, String contents,
587 String[] headers) {
588 String query = method + " " + addr + " HTTP/1.1";
589 for (int i = 0; i < headers.length; i++) {
590 query += "\n" + headers[i];
592 if (contents != null) {
593 query += "\nContent-Length: " + contents.getBytes().length;
594 } else {
595 query += "\nContent-Length: 0";
597 query += "\nUser-Agent: " + USER_AGENT + "\nHost: " + host + ":" + po +
598 "\n\n";
599 // "Host: " + host + "\n\n\n";
600 if (contents != null) {
601 query += contents;
602 } else {
603 query += "\n\n";
605 return query.getBytes();