1 /* BionicMessage.net Java GroupDAV library V1.0
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
;
29 import net
.bionicmessage
.extutils
.*;
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
;
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 * "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
;
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
) {
110 if (url
.indexOf("https://") != -1) {
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();
123 base64cache
= "Authorization: Basic " + Base64
.encodeBytes(new String(u
+":"+p
).getBytes());
126 /** Set the logger to output to
127 * @param log The logger instance to use
129 public void setLogger(Logger log
) {
132 /** Initiate client */
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();
143 sbuf
= new StringBuffer();
144 // sock = createSocket();
145 //sock.setKeepAlive(true);
146 // setupReadThread();
147 // th.setDaemon(true);
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
);
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 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
);
190 locations
.put(url
,gtype
);
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
);
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 */
221 /** Posts an object */
222 public GroupDAVObject
postObject(String subdir
, String uid
, String contents
) throws Exception
{
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",
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
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());
259 public GroupDAVObject
modifyObject(String addr
,
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
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());
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
);
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
);
316 /** TODO: Do a GroupDAV compliant PROPFIND */
317 public byte[] buildPROPFIND(String subdir
) {
318 String gdir
= sdir
+ "/";
319 if (subdir
.indexOf(sdir
) > -1) {
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" +
331 return PROPFIND
.getBytes();
334 /* public byte[] buildPROPFINDList(String subdir) {
335 String gdir = sdir + "/";
336 if (subdir.indexOf(sdir) > -1) {
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" +
349 return PROPFIND.getBytes();
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
,
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
);
373 if (etags
.get(objectu
) != null) {
374 eTag
= (String
)etags
.get(objectu
); } else {
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);
386 String object
="BEGIN:VCALENDAR\n" +
387 "PRODID:-//BM GroupDAV Client//Test Creation Object/EN\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" +
399 "ORGANIZER:MAILTO:matt@comalies\n" +
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;
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
) {
437 public void startDocument() throws SAXException
{
438 charBuffers
= new Hashtable();
439 element_names
= new Hashtable();
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
{
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
);
461 charBuffers
.put(key
,new StringBuffer());
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")) {
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() {
514 public Hashtable
getObjectEtags() {
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
{
531 st
= ssf
.createSocket(host
,po
);
533 st
= new Socket(host
,po
);
538 private String
readChars() throws Exception
{
539 th
.sleep(sock
.getSoTimeout());
540 String chars
= sbuf
.toString();
542 sbuf
= new StringBuffer();
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();
557 private String
sendNonKeepAliveRequest(byte[] by
) throws Exception
{
558 String dbugstring
= new String(by
);
559 logger
.finer("We sent:\r\n"+dbugstring
);
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
));
569 char[] cbuf
= new char[300];
570 while((i
= rb
.read(cbuf
)) != -1) {
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
,
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
;
595 query
+= "\nContent-Length: 0";
597 query
+= "\nUser-Agent: " + USER_AGENT
+ "\nHost: " + host
+ ":" + po
+
599 // "Host: " + host + "\n\n\n";
600 if (contents
!= null) {
605 return query
.getBytes();