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 java
.nio
.ByteBuffer
;
30 import net
.bionicmessage
.extutils
.*;
32 import java
.util
.logging
.*;
33 import javax
.xml
.parsers
.*;
34 import org
.xml
.sax
.InputSource
;
35 import org
.xml
.sax
.helpers
.DefaultHandler
;
36 import javax
.net
.SocketFactory
;
37 import javax
.net
.ssl
.SSLSocketFactory
;
38 import org
.xml
.sax
.Attributes
;
39 import org
.xml
.sax
.SAXException
;
42 * A basic GroupDAV implementation
43 * @author <a href="http://bionicmessage.net">Mathew McBride</a>.
45 public class groupDAV
{
47 private String u
= "";
48 private String p
= "";
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 /* At the moment we only support one set of folders each. */
58 private String vtodoloc
= "";
59 private String vcalloc
= "";
60 private ArrayList vcallocs
= null;
61 private String vaddrloc
= "";
62 private SocketFactory ssf
= null;
63 private boolean ssl
= false;
64 private Socket sock
= null;
65 private int offset
= 0;
66 private StringBuffer sbuf
= null;
67 private Thread th
= null;
68 private static final String USER_AGENT
= "BionicMessage.net GroupDAV {0.9;Java}";
69 private String tok
= "http://";
70 private String origurl
= "";
71 private entityFinderHandler storeFinderHandler
= null;
72 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 * "Autorization: Basic pass:user"
81 * (Google HTTP Basic authentication for more information)
83 public groupDAV(String url
, String b64cache
) {
85 if (!origurl
.substring(origurl
.length() - 1).equals("/")) {
88 if (url
.indexOf("https://") != -1) {
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();
99 base64cache
= "Authorization: Basic " + b64cache
;
103 /** Constructor using a http://// url, and plain text passwords.
104 * @param url The host and port of the server to connect to, in the
105 * format of http://hostname:port/
106 * @param user The username of the user connecting to the server
107 * @param pass The password of the user connecting to the server
109 public groupDAV(String url
, String user
, String pass
) {
111 if (url
.indexOf("https://") != -1) {
115 url
= url
.replaceAll(tok
, ""); // goodbye http://
116 String hostname
= url
.split(":")[0];
117 String ports
= url
.split(":")[1]; // port and url
118 ports
= ports
.replace("/", "");
119 int port
= Integer
.valueOf(ports
).intValue();
124 base64cache
= "Authorization: Basic " + Base64
.encodeBytes(new String(u
+ ":" + p
).getBytes());
128 /** Set the logger to output to
129 * @param log The logger instance to use
131 public void setLogger(Logger log
) {
135 /** Initiate client */
137 logger
.setLevel(Level
.ALL
);
138 logger
.addHandler(ch
);
139 ch
.setLevel(Level
.ALL
);
140 logger
.info("GroupDAV client init()");
141 storeFinderHandler
= new entityFinderHandler(logger
);
142 dirFinderHandler
= new entityFinderHandler(logger
);
144 ssf
= SSLSocketFactory
.getDefault();
146 sbuf
= new StringBuffer();
147 // sock = createSocket();
148 //sock.setKeepAlive(true);
149 // setupReadThread();
150 // th.setDaemon(true);
153 } catch (Exception ex
) {
154 ex
.printStackTrace();
158 /** Send after client initation to discover object stores on the server
159 * @throws Exception When the server fails to supply discovery data */
160 public boolean findStores() throws Exception
{
161 byte[] pfind
= buildPROPFIND("");
162 String output
= sendNonKeepAliveRequest(pfind
);
163 int clength
= findContentLength(output
);
165 logger
.warning("No Content-Length in findStores");
167 String xmldata
= output
.substring(output
.length() - clength
- 1, output
.length());
168 xmldata
= xmldata
.trim();
169 if (!xmldata
.contains("<?xml")) {
170 throw new Exception("No <?xml field in xmldata, dying");
172 logger
.finer("Split=" + xmldata
);
173 generateEntityList(xmldata
, storeFinderHandler
);
177 public entityFinderHandler
getDirFinderHandler() {
178 return dirFinderHandler
;
181 public entityFinderHandler
getStoreFinderHandler() {
182 return storeFinderHandler
;
185 /** Get a list of store URL's from the server */
186 public Hashtable
getURLLocs() {
187 Hashtable locations
= new Hashtable();
188 ArrayList list
= storeFinderHandler
.getObjectList();
189 Hashtable dtypes
= storeFinderHandler
.getObjectDtypes();
190 Hashtable gtypes
= storeFinderHandler
.getObjectGtypes();
191 for (int i
= 0; i
< list
.size(); i
++) {
192 String url
= (String
) list
.get(0);
193 String davtype
= (String
) dtypes
.get(url
);
194 if (davtype
.equals("collection")) {
195 String gtype
= (String
) gtypes
.get(url
);
197 locations
.put(url
, gtype
);
204 /** List objects in a store
206 public List
listObjects(String url
) throws Exception
{
207 byte[] pfind
= buildPROPFIND(url
);
208 String output
= sendNonKeepAliveRequest(pfind
);
209 int clength
= findContentLength(output
);
211 logger
.warning("We don't have a Content Length field in listObjects");
213 String xmldata
= output
.toString().split("\r\n\r\n")[1];
214 generateEntityList(xmldata
, dirFinderHandler
);
215 ArrayList objectList
= dirFinderHandler
.getObjectList();
216 Hashtable types
= dirFinderHandler
.getObjectDtypes();
217 ArrayList cleaned
= new ArrayList();
218 for (int i
= 0; i
< objectList
.size(); i
++) {
219 String lurl
= (String
) objectList
.get(i
);
220 String type
= (String
) types
.get(lurl
);
221 if (type
!= null && type
.contains("collection")) {
222 /* Don't add this location to the list; it is a collection */
230 /** Posts an object */
231 public GroupDAVObject
postObject(String subdir
, String uid
, String contents
) throws Exception
{
233 String type
= "text/calendar";
234 if (contents
.indexOf("begin:vcard") != -1 ||
235 contents
.indexOf("BEGIN:VCARD") != -1) {
236 type
= "text/x-vcard";
238 String
[] headers
= new String
[]{"Content-Type: " + type
+ "; charset=utf-8",
242 byte[] query
= buildGroupDAVQuery("PUT", subdir
+ "new.ics", contents
, headers
);
243 String q
= new String(query
);
244 String t
= sendNonKeepAliveRequest(query
);
245 logger
.fine("We got" + t
);
246 String firstLine
= t
.split("\r\n")[0];
247 if (firstLine
.indexOf("405") != -1) {
248 throw new Exception("Post error: " + t
);
250 GroupDAVObject gobjP
= new GroupDAVObject(t
, GroupDAVObject
.OBJECT_PUT
);
251 /* Did the server give us a location? If not, assume the location is exactly
253 if (gobjP
.getLocation() == null) {
254 gobjP
.setLocation(origurl
+ subdir
+ uid
);
255 } else if (gobjP
.getLocation() != null) {
256 // String strippedurl = gobjP.getLocation().replace(tok+host+":"+po,"");
257 // gobjP.setLocation(strippedurl);
259 // Did the server give us an eTag with the PUT request or will we have
260 // to fetch the object?
261 if (gobjP
.getEtag() == null) {
262 GroupDAVObject gobjG
= getObject(gobjP
.getLocation());
269 public GroupDAVObject
modifyObject(String addr
,
271 String etag
) throws Exception
{
272 String type
= "text/calendar";
273 if (contents
.indexOf("begin:vcard") != -1 ||
274 contents
.indexOf("BEGIN:VCARD") != -1) {
275 type
= "text/x-vcard";
277 // Drop the hostname since we specify it in Host:
278 //addr = addr.replace(origurl, "/");
279 URL objAddr
= new URL(addr
);
281 String
[] header
= new String
[]{base64cache
,
282 "Content-Type: " + type
+ "; charset=utf-8", "If-Match:" + etag
284 byte[] query
= buildGroupDAVQuery("PUT", objAddr
.getPath(), contents
, header
);
285 String q
= new String(query
);
286 String resp
= sendNonKeepAliveRequest(query
);
287 logger
.finest("We got: " + resp
);
288 String firstLine
= resp
.split("\r\n")[0];
289 if (firstLine
.indexOf("405") != -1) {
290 throw new Exception("Modify error:" + resp
);
292 GroupDAVObject gobjP
= new GroupDAVObject(resp
, GroupDAVObject
.OBJECT_PUT
);
293 /* Did the server give us a location? If not, assume the location is exactly
295 if (gobjP
.getLocation() == null) {
296 gobjP
.setLocation(addr
);
297 } else if (gobjP
.getLocation() != null) {
298 String strippedurl
= gobjP
.getLocation().replace(tok
+ host
+ ":" + po
, "");
299 gobjP
.setLocation(strippedurl
);
301 // Did the server give us an eTag with the PUT request or will we have
302 // to fetch the object?
303 if (gobjP
.getEtag() == null) {
304 GroupDAVObject gobjG
= getObject(gobjP
.getLocation());
311 public GroupDAVObject
deleteObject(String addr
, String etag
) throws Exception
{
312 URL netURL
= new URL(addr
);
313 String
[] header
= new String
[]{base64cache
, "If-Match:" + etag
};
314 byte[] query
= buildGroupDAVQuery("DELETE", netURL
.getPath(), null, header
);
315 String resp
= sendNonKeepAliveRequest(query
);
316 logger
.finest("We got: " + resp
);
317 GroupDAVObject gbo
= new GroupDAVObject(resp
, GroupDAVObject
.OBJECT_KILLED
);
321 /** Execute a GET command and return a GroupDAVObject */
322 public GroupDAVObject
getObject(String url
) throws Exception
{
323 //url = url.replace(tok + host + ":" + po, "");
324 URL pathURL
= new URL(url
);
325 String header
[] = {base64cache
};
326 byte[] send
= buildGroupDAVQuery("GET", pathURL
.getPath(), null, header
);
327 String st
= new String(send
);
328 String t
= sendNonKeepAliveRequest(send
);
329 String firstLine
= t
.split("\r\n")[0];
330 if (firstLine
.contains("400")) {
331 throw new Exception("HTTP Exception encountered in getObject: " + firstLine
);
333 GroupDAVObject obj
= new GroupDAVObject(t
, GroupDAVObject
.OBJECT_GET
);
337 /** TODO: Do a GroupDAV compliant PROPFIND */
338 public byte[] buildPROPFIND(String subdir
) {
339 String gdir
= sdir
+ "/";
340 if (subdir
.indexOf(sdir
) > -1) {
343 String XMLSTRING
= "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
344 "<propfind xmlns=\"DAV:\"><allprop/><prop><getetag/></prop></propfind>";
345 // String XMLSTRING = "";
346 String PROPFIND
= "PROPFIND " + gdir
+ subdir
+ " HTTP/1.1\n" +
347 "Cache-control: no-cache\nPragma: no-cache\nAccept-Language: en\n" +
348 base64cache
+ "\nContent-Length: " +
349 XMLSTRING
.getBytes().length
+ "\nHost: " + host
+ ":" + po
+ "\n" +
350 "Depth: 1\nContent-Type: text/xml;charset=utf-8\nAccept: text/*\n\n" +
352 return PROPFIND
.getBytes();
355 /* public byte[] buildPROPFINDList(String subdir) {
356 String gdir = sdir + "/";
357 if (subdir.indexOf(sdir) > -1) {
360 String XMLSTRING = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
361 "<propfind xmlns=\"DAV:\">" +
362 "<prop xmlns=\"DAV:\"><allprop xmlns=\"DAV:\"/><getetag xmlns=\"DAV:\"/>" +
363 "</prop></propfind>";
364 String PROPFIND = "PROPFIND " + gdir + subdir + " HTTP/1.1\n" +
365 "Cache-control: no-cache\nPragma: no-cache\nAccept-Language: en\n" +
366 base64cache + "\nContent-Length: " +
367 XMLSTRING.getBytes().length + "\nHost: " + host + ":" + po + "\n" +
368 "Content-Type: text/xml;charset=utf-8\nAccept: text/*\n\n" +
370 return PROPFIND.getBytes();
374 private void generateEntityList(String xmldata
, entityFinderHandler efh
) throws Exception
{
375 org
.apache
.xerces
.parsers
.SAXParser sparser
= new org
.apache
.xerces
.parsers
.SAXParser();
376 sparser
.setFeature("http://xml.org/sax/features/namespaces", true);
377 sparser
.setContentHandler(efh
);
378 sparser
.parse(new InputSource(new StringReader(xmldata
)));
381 /** Testing method. Edit source and replace with own
382 * server settings to test client. */
383 public static void main(String args
[]) {
384 /* Todo: use GUI to ask for info */
385 String SERVER
= "http://your.server.here:80";
386 groupDAV client
= new groupDAV(SERVER
,
390 List calobjects
= client
.listObjects("/groupdav/Calendar/");
391 Map etags
= client
.getEtags();
392 for (int i
= 0; i
< calobjects
.size(); i
++) {
393 String objectu
= (String
) calobjects
.get(i
);
395 if (etags
.get(objectu
) != null) {
396 eTag
= (String
) etags
.get(objectu
);
400 GroupDAVObject gobj
= client
.getObject(objectu
);
401 System
.err
.println(gobj
.getContent() + "\n|etag-server=" + eTag
);
402 System
.err
.println("|etag-object=" + gobj
.getEtag());
403 String object
= gobj
.getContent();
406 java
.util
.Random rd
= new java
.util
.Random();
407 int uid
= rd
.nextInt(32);
409 String object
= "BEGIN:VCALENDAR\n" +
410 "PRODID:-//BM GroupDAV Client//Test Creation Object/EN\n" +
414 "SUMMARY:Test object\n" +
415 "LOCATION:In dev slash null\n" +
416 "DESCRIPTION:Please don't fail me! I'm innocent!\n" +
417 "DTSTART:20060408T100059Z\n" +
418 "DTEND:20060408T110059Z\n" +
422 "ORGANIZER:MAILTO:matt@comalies\n" +
426 // GroupDAVObject gbo = client.postObject(calendarUrl,u,object);
427 //System.out.println("Etag for new object= " + gbo.getEtag());
428 // Test change operation
429 // object.replaceAll("don't","never");
430 // GroupDAVObject gboc = client.modifyObject(gbo.getLocation(),object,gbo.getEtag());
431 // GroupDAVObject gbod = client.deleteObject(gbo.getLocation(),gbo.getEtag());
432 } catch (Exception ex
) {
434 ex
.printStackTrace();
438 /** Parses the result of PROPFIND */
439 private class entityFinderHandler
extends DefaultHandler
{
441 Integer element_index
;
442 Stack parents
= null;
443 Hashtable charBuffers
= null;
444 Hashtable element_names
= null;
446 String obj_href
= ""; // DAV Object href
447 String obj_dtype
= ""; // DAV resource type
448 String obj_gtype
= ""; // GroupDAV resource type
449 String obj_status
= ""; // DAV object status
450 String obj_etag
= ""; // DAV Object etag
451 Hashtable object_dtypes
= null;
452 Hashtable object_gtypes
= null;
453 Hashtable object_etags
= null;
454 Hashtable object_statuses
= null;
455 ArrayList object_list
= null;
457 /** Creates a new instance of entityFinderHandler */
458 public entityFinderHandler(Logger l
) {
462 public void startDocument() throws SAXException
{
463 charBuffers
= new Hashtable();
464 element_names
= new Hashtable();
466 parents
= new Stack();
467 parents
.push(new Integer(element_index
));
468 object_dtypes
= new Hashtable();
469 object_gtypes
= new Hashtable();
470 object_etags
= new Hashtable();
471 object_statuses
= new Hashtable();
472 object_list
= new ArrayList();
475 public void endDocument() throws SAXException
{
478 public void startElement(String uri
, String localName
, String qName
, Attributes atts
) throws SAXException
{
480 int parentNodeIx
= ((Integer
) parents
.peek()).intValue();
481 // this element in turn becomes the parent for subsequent routines
482 Integer key
= new Integer(element_index
);
483 element_names
.put(key
, qName
);
485 charBuffers
.put(key
, new StringBuffer());
489 public void endElement(String uri
, String localName
, String qName
) throws SAXException
{
490 Integer key
= (Integer
) parents
.pop();
491 StringBuffer charBuffer
= (StringBuffer
) charBuffers
.get(key
);
492 if (uri
.contains("groupdav.org")) { // GroupDAV namespace
493 if (localName
.contains("vevent-collection") ||
494 localName
.contains("vtodo-collection") ||
495 localName
.contains("vcard-collection")) {
496 obj_gtype
= localName
;
498 } else if (uri
.contains("DAV:")) { // DAV namespace
499 if (localName
.contains("collection")) {
500 obj_dtype
= localName
;
501 } else if (localName
.contains("status")) {
502 obj_status
= charBuffer
.toString();
503 } else if (localName
.contains("href")) {
504 obj_href
= charBuffer
.toString();
505 } else if (localName
.contains("getetag")) {
506 obj_etag
= charBuffer
.toString();
509 if (uri
.contains("DAV:") && localName
.contains("response")) {
510 // Stuff all the known data about the object into the collections
511 object_list
.add(obj_href
);
512 object_etags
.put(obj_href
, obj_etag
);
513 object_statuses
.put(obj_href
, obj_status
);
514 object_dtypes
.put(obj_href
, obj_dtype
);
515 object_gtypes
.put(obj_href
, obj_gtype
);
517 /* Reset ready for the next one */
518 if (localName
.contains("response")) {
527 public void characters(char[] ch
, int start
, int length
) throws SAXException
{
528 String chars
= new String(ch
, start
, length
);
529 if (chars
.trim().length() > 0) {
530 Integer parent
= (Integer
) parents
.peek();
531 StringBuffer charBuffer
= (StringBuffer
) charBuffers
.get(parent
);
532 charBuffer
.append(chars
);
536 public ArrayList
getObjectList() {
540 public Hashtable
getObjectEtags() {
544 public Hashtable
getObjectStatuses() {
545 return object_statuses
;
548 public Hashtable
getObjectDtypes() {
549 return object_dtypes
;
552 public Hashtable
getObjectGtypes() {
553 return object_gtypes
;
557 /** Create a socket to the host */
558 private Socket
createSocket() throws Exception
{
561 st
= ssf
.createSocket(host
, po
);
563 st
= new Socket(host
, po
);
568 private String
readChars() throws Exception
{
569 th
.sleep(sock
.getSoTimeout());
570 String chars
= sbuf
.toString();
572 sbuf
= new StringBuffer();
576 private int findContentLength(String data
) {
577 String
[] headers
= data
.split("\n");
578 for (int i
= 0; i
< headers
.length
; i
++) {
579 String curHeader
= headers
[i
];
580 String
[] line
= curHeader
.split(":");
581 if (line
[0].equalsIgnoreCase("Content-Length")) {
582 Integer it
= new Integer(line
[1].trim());
583 return it
.intValue();
589 private String
sendNonKeepAliveRequest(byte[] by
) throws Exception
{
590 String dbugstring
= new String(by
);
591 logger
.fine(dbugstring
);
592 String loggableString
= dbugstring
.replace(base64cache
, "Authorization: Basic [removed]");
593 logger
.finer("We sent:\r\n" + loggableString
);
595 long time
= new java
.util
.Date().getTime();
596 Socket st
= createSocket();
597 st
.getOutputStream().write(by
);
598 st
.getOutputStream().flush();
599 InputStream sc
= st
.getInputStream();
600 BufferedInputStream bis
= new BufferedInputStream(sc
);
602 StringBuffer sb
= new StringBuffer();
606 // We need to process the message character by chacter instead of byte by byte
607 // or otherwise 2-4 byte characters (many Unicode characters) get stripped away.
608 // To do this, we use the InputStreamReader class to convert byte stream into
609 // an Unicode character stream.
610 BufferedReader charstream
= new BufferedReader(new InputStreamReader(bis
, "UTF-8"));
613 char[] buf
= new char[300];
614 op
= charstream
.read(buf
);
615 if (op
!= 0 && op
!= -1) {
616 for (int i
= 0; i
< op
; i
++) {
618 // Strip bytes which would foul an XML parser, i.e SIF
619 // for Funambol upstream. See
620 // http://cse-mjmcl.cse.bris.ac.uk/blog/2007/02/14/1171465494443.html
624 ((c
>= 0x20) && (c
<= 0xD7FF)) ||
625 ((c
>= 0xE000) && (c
<= 0xFFFD)) ||
626 ((c
>= 0x10000) && (c
<= 0x10FFFF))) {
636 logger
.fine(fouls
+ " invalid (XML-wise) characters detected in stream. Is the source/users clean?");
640 long closetime
= new java
.util
.Date().getTime();
641 long rtt
= closetime
- time
;
642 logger
.finer("We got:\r\n" + sb
.toString() + "\r\n....in " + rtt
+ "ms");
643 return sb
.toString();
646 /** Returns a list of HTTP/DAV etags. Object URL's are the key in the Map.
647 * If the DAV PROPFIND did not return etag properties in its result
648 * an attempt to get() a etag will return null as per Map implemementation rules
650 public Map
getEtags() {
651 return dirFinderHandler
.getObjectEtags();
654 private byte[] buildGroupDAVQuery(String method
, String addr
, String contents
,
656 String query
= method
+ " " + addr
+ " HTTP/1.1";
658 headers
.length
; i
++) {
659 query
+= "\n" + headers
[i
];
662 if (contents
!= null) {
663 query
+= "\nContent-Length: " + contents
.getBytes().length
;
665 query
+= "\nContent-Length: 0";
668 query
+= "\nUser-Agent: " + USER_AGENT
+ "\nHost: " + host
+ ":" + po
+
670 // "Host: " + host + "\n\n\n";
671 if (contents
!= null) {
677 return query
.getBytes();