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 host
= "";
48 private int po
= 2000;
49 private String base64cache
= "";
50 private String sdir
= "";
51 private static Logger logger
=
52 Logger
.getLogger(("funambol"));
53 private static ConsoleHandler ch
= new ConsoleHandler();
54 DocumentBuilderFactory fdb
= DocumentBuilderFactory
.newInstance();
55 private SocketFactory ssf
= null;
56 private boolean ssl
= false;
57 private Socket sock
= null;
58 private int offset
= 0;
59 private StringBuffer sbuf
= null;
60 private Thread th
= null;
61 private static final String USER_AGENT
= "BionicMessage.net GroupDAV {0.9;Java}";
62 private String tok
= "http://";
63 private String origurl
= "";
64 private entityFinderHandler storeFinderHandler
= null;
65 private entityFinderHandler dirFinderHandler
= null;
67 private URL serverURL
= null;
69 /** For use in situations where something (i.e in the case of
70 * Funambol's authentication) has already given us a HTTP Basic Base64
71 * representation of user:pass
72 * @param url The host and port of the server to connect to, in the
73 * format of http://hostname:port/
74 * @param b64cache A String pre-encoded in Base 64 representing
75 * "Autorization: Basic pass:user"
76 * (Google HTTP Basic authentication for more information)
78 public groupDAV(String url
, String b64cache
) {
81 serverURL
= new URL(url
);
82 if (serverURL
.getProtocol().contains("https")) {
86 host
= serverURL
.getHost();
87 po
= serverURL
.getPort();
91 } catch (MalformedURLException ex
) {
92 Logger
.getLogger(groupDAV
.class.getName()).log(Level
.SEVERE
, null, ex
);
93 throw new IllegalArgumentException(ex
);
95 if (!origurl
.substring(origurl
.length() - 1).equals("/")) {
98 if (url
.indexOf("https://") != -1) {
102 base64cache
= "Authorization: Basic " + b64cache
;
106 /** Constructor using a http://// url, and plain text passwords.
107 * @param url The host and port of the server to connect to, in the
108 * format of http://hostname:port/
109 * @param user The username of the user connecting to the server
110 * @param pass The password of the user connecting to the server
112 public groupDAV(String url
, String user
, String pass
) {
115 serverURL
= new URL(url
);
116 if (serverURL
.getProtocol().contains("https")) {
120 host
= serverURL
.getHost();
121 po
= serverURL
.getPort();
125 } catch (MalformedURLException ex
) {
126 Logger
.getLogger(groupDAV
.class.getName()).log(Level
.SEVERE
, null, ex
);
127 throw new IllegalArgumentException(ex
);
129 if (!origurl
.substring(origurl
.length() - 1).equals("/")) {
132 if (url
.indexOf("https://") != -1) {
136 base64cache
= "Authorization: Basic " + Base64
.encodeBytes(new String(user
+ ":" + pass
).getBytes());
140 /** Set the logger to output to
141 * @param log The logger instance to use
143 public void setLogger(Logger log
) {
147 /** Initiate client */
149 logger
.setLevel(Level
.ALL
);
150 logger
.addHandler(ch
);
151 ch
.setLevel(Level
.ALL
);
152 logger
.info("GroupDAV client init()");
153 storeFinderHandler
= new entityFinderHandler(logger
);
154 dirFinderHandler
= new entityFinderHandler(logger
);
156 ssf
= SSLSocketFactory
.getDefault();
158 sbuf
= new StringBuffer();
159 // sock = createSocket();
160 //sock.setKeepAlive(true);
161 // setupReadThread();
162 // th.setDaemon(true);
165 } catch (Exception ex
) {
166 ex
.printStackTrace();
170 /** Send after client initation to discover object stores on the server
171 * @throws Exception When the server fails to supply discovery data */
172 public boolean findStores() throws Exception
{
173 byte[] pfind
= buildPROPFIND("");
174 String output
= sendNonKeepAliveRequest(pfind
);
175 int clength
= findContentLength(output
);
177 logger
.warning("No Content-Length in findStores");
179 String xmldata
= output
.substring(output
.length() - clength
- 1, output
.length());
180 xmldata
= xmldata
.trim();
181 if (!xmldata
.contains("<?xml")) {
182 throw new Exception("No <?xml field in xmldata, dying");
184 logger
.finer("Split=" + xmldata
);
185 generateEntityList(xmldata
, storeFinderHandler
);
189 public entityFinderHandler
getDirFinderHandler() {
190 return dirFinderHandler
;
193 public entityFinderHandler
getStoreFinderHandler() {
194 return storeFinderHandler
;
197 /** Get a list of store URL's from the server */
198 public Hashtable
getURLLocs() {
199 Hashtable locations
= new Hashtable();
200 ArrayList list
= storeFinderHandler
.getObjectList();
201 Hashtable dtypes
= storeFinderHandler
.getObjectDtypes();
202 Hashtable gtypes
= storeFinderHandler
.getObjectGtypes();
203 for (int i
= 0; i
< list
.size(); i
++) {
204 String url
= (String
) list
.get(0);
205 String davtype
= (String
) dtypes
.get(url
);
206 if (davtype
.equals("collection")) {
207 String gtype
= (String
) gtypes
.get(url
);
209 locations
.put(url
, gtype
);
216 /** List objects in a store
218 public List
listObjects(String url
) throws Exception
{
219 byte[] pfind
= buildPROPFIND(url
);
220 String output
= sendNonKeepAliveRequest(pfind
);
221 int clength
= findContentLength(output
);
223 logger
.warning("We don't have a Content Length field in listObjects");
224 /* Temporary, ugly workaround for chunked transfer. Only use if
226 if (output.endsWith("0")) {
227 int startOfXML = output.indexOf("<?xml");
228 output = output.substring(startOfXML, output.lastIndexOf("0"));
232 String xmldata
= output
.toString().split("\r\n\r\n")[1];
233 generateEntityList(xmldata
, dirFinderHandler
);
234 ArrayList objectList
= dirFinderHandler
.getObjectList();
235 Hashtable types
= dirFinderHandler
.getObjectDtypes();
236 Hashtable etags
= dirFinderHandler
.getObjectEtags();
237 ArrayList cleaned
= new ArrayList();
238 for (int i
= 0; i
< objectList
.size(); i
++) {
239 String lurl
= (String
) objectList
.get(i
);
240 String type
= (String
) types
.get(lurl
);
241 String etag
= (String
) etags
.get(lurl
);
242 if (type
!= null && type
.contains("collection") | etag
.length() < 1) {
243 /* Don't add this location to the list; it is a collection */
251 /** Posts an object */
252 public GroupDAVObject
postObject(String subdir
, String uid
, String contents
) throws Exception
{
254 String type
= "text/calendar";
255 if (contents
.indexOf("begin:vcard") != -1 ||
256 contents
.indexOf("BEGIN:VCARD") != -1) {
257 type
= "text/x-vcard";
259 String
[] headers
= new String
[]{"Content-Type: " + type
+ "; charset=utf-8",
263 byte[] query
= buildGroupDAVQuery("PUT", subdir
+ "new.ics", contents
, headers
);
264 String q
= new String(query
);
265 String t
= sendNonKeepAliveRequest(query
);
266 logger
.fine("We got" + t
);
267 String firstLine
= t
.split("\r\n")[0];
268 if (firstLine
.indexOf("405") != -1) {
269 throw new Exception("Post error: " + t
);
271 GroupDAVObject gobjP
= new GroupDAVObject(t
, GroupDAVObject
.OBJECT_PUT
);
272 /* Did the server give us a location? If not, assume the location is exactly
273 * what we gave it (this rarely happens anyway) */
274 if (gobjP
.getLocation() == null) {
275 gobjP
.setLocation(origurl
+ subdir
+ uid
);
276 } else if (gobjP
.getLocation() != null) {
277 // String strippedurl = gobjP.getLocation().replace(tok+host+":"+po,"");
278 // gobjP.setLocation(strippedurl);
280 // Did the server give us an eTag with the PUT request or will we have
281 // to fetch the object?
282 if (gobjP
.getEtag() == null) {
283 GroupDAVObject gobjG
= getObject(gobjP
.getLocation());
289 /** Modifies an object on the server.
290 * @param addr Path to object on server (i.e /groupdav/Calendar/event.ics)
291 * @param contents New object contents
292 * @param etag Previous etag of object
293 * @return A GroupDAVObject with returned server contents
294 * @throws java.lang.Exception Due to network errors
296 public GroupDAVObject
modifyObject(String addr
,
298 String etag
) throws Exception
{
299 String type
= "text/calendar";
300 if (contents
.indexOf("begin:vcard") != -1 ||
301 contents
.indexOf("BEGIN:VCARD") != -1) {
302 type
= "text/x-vcard";
304 // Drop the hostname since we specify it in Host:
305 //addr = addr.replace(origurl, "/");
306 URL objAddr
= new URL(serverURL
, addr
);
308 String
[] header
= new String
[]{base64cache
,
309 "Content-Type: " + type
+ "; charset=utf-8", "If-Match:" + etag
311 byte[] query
= buildGroupDAVQuery("PUT", objAddr
.getPath(), contents
, header
);
312 String q
= new String(query
);
313 String resp
= sendNonKeepAliveRequest(query
);
314 logger
.finest("We got: " + resp
);
315 String firstLine
= resp
.split("\r\n")[0];
316 if (firstLine
.indexOf("405") != -1) {
317 throw new Exception("Modify error:" + resp
);
319 GroupDAVObject gobjP
= new GroupDAVObject(resp
, GroupDAVObject
.OBJECT_PUT
);
320 /* Did the server give us a location? If not, assume the location is exactly
322 if (gobjP
.getLocation() == null) {
323 gobjP
.setLocation(addr
);
324 } else if (gobjP
.getLocation() != null) {
325 String strippedurl
= gobjP
.getLocation().replace(tok
+ host
+ ":" + po
, "");
326 gobjP
.setLocation(strippedurl
);
328 // Did the server give us an eTag with the PUT request or will we have
329 // to fetch the object?
330 if (gobjP
.getEtag() == null) {
331 GroupDAVObject gobjG
= getObject(gobjP
.getLocation());
338 public GroupDAVObject
deleteObject(String addr
, String etag
) throws Exception
{
339 URL netURL
= new URL(serverURL
, addr
);
340 String
[] header
= new String
[]{base64cache
, "If-Match:" + etag
};
341 byte[] query
= buildGroupDAVQuery("DELETE", netURL
.getPath(), null, header
);
342 String resp
= sendNonKeepAliveRequest(query
);
343 logger
.finest("We got: " + resp
);
344 GroupDAVObject gbo
= new GroupDAVObject(resp
, GroupDAVObject
.OBJECT_KILLED
);
348 /** Execute a GET command and return a GroupDAVObject */
349 public GroupDAVObject
getObject(String url
) throws Exception
{
350 //url = url.replace(tok + host + ":" + po, "");
351 URL pathURL
= new URL(serverURL
, url
);
352 String header
[] = {base64cache
};
353 byte[] send
= buildGroupDAVQuery("GET", pathURL
.getPath(), null, header
);
354 String st
= new String(send
);
355 String t
= sendNonKeepAliveRequest(send
);
356 String firstLine
= t
.split("\r\n")[0];
357 if (firstLine
.contains("400")) {
358 throw new Exception("HTTP Exception encountered in getObject: " + firstLine
);
360 GroupDAVObject obj
= new GroupDAVObject(t
, GroupDAVObject
.OBJECT_GET
);
364 /** Build a PROPFIND query to list the contents of a collection
366 * @param subdir The server path to query
367 * @return A Byte array containing the HTTP headers and DAV query.
369 public byte[] buildPROPFIND(String subdir
) {
370 String gdir
= sdir
+ "/";
371 if (subdir
.indexOf(sdir
) > -1) {
374 String XMLSTRING
= "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
375 "<propfind xmlns=\"DAV:\"><allprop/><prop><getetag/></prop></propfind>";
376 // String XMLSTRING = "";
377 String PROPFIND
= "PROPFIND " + gdir
+ subdir
+ " HTTP/1.1\n" +
378 "Cache-control: no-cache\nPragma: no-cache\nAccept-Language: en\n" +
379 base64cache
+ "\nContent-Length: " +
380 XMLSTRING
.getBytes().length
+ "\nHost: " + host
+ "\n" +
381 "Depth: 1\nContent-Type: text/xml;charset=utf-8\nAccept: text/*\n\n" +
383 return PROPFIND
.getBytes();
386 /* public byte[] buildPROPFINDList(String subdir) {
387 String gdir = sdir + "/";
388 if (subdir.indexOf(sdir) > -1) {
391 String XMLSTRING = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
392 "<propfind xmlns=\"DAV:\">" +
393 "<prop xmlns=\"DAV:\"><allprop xmlns=\"DAV:\"/><getetag xmlns=\"DAV:\"/>" +
394 "</prop></propfind>";
395 String PROPFIND = "PROPFIND " + gdir + subdir + " HTTP/1.1\n" +
396 "Cache-control: no-cache\nPragma: no-cache\nAccept-Language: en\n" +
397 base64cache + "\nContent-Length: " +
398 XMLSTRING.getBytes().length + "\nHost: " + host + ":" + po + "\n" +
399 "Content-Type: text/xml;charset=utf-8\nAccept: text/*\n\n" +
401 return PROPFIND.getBytes();
405 private void generateEntityList(String xmldata
, entityFinderHandler efh
) throws Exception
{
406 org
.apache
.xerces
.parsers
.SAXParser sparser
= new org
.apache
.xerces
.parsers
.SAXParser();
407 sparser
.setFeature("http://xml.org/sax/features/namespaces", true);
408 sparser
.setContentHandler(efh
);
409 sparser
.parse(new InputSource(new StringReader(xmldata
)));
412 /** Parses the result of PROPFIND */
413 private class entityFinderHandler
extends DefaultHandler
{
415 Integer element_index
;
416 Stack parents
= null;
417 Hashtable charBuffers
= null;
418 Hashtable element_names
= null;
420 String obj_href
= ""; // DAV Object href
421 String obj_dtype
= ""; // DAV resource type
422 String obj_gtype
= ""; // GroupDAV resource type
423 String obj_status
= ""; // DAV object status
424 String obj_etag
= ""; // DAV Object etag
425 Hashtable object_dtypes
= null;
426 Hashtable object_gtypes
= null;
427 Hashtable object_etags
= null;
428 Hashtable object_statuses
= null;
429 ArrayList object_list
= null;
431 /** Creates a new instance of entityFinderHandler */
432 public entityFinderHandler(Logger l
) {
436 public void startDocument() throws SAXException
{
437 charBuffers
= new Hashtable();
438 element_names
= new Hashtable();
440 parents
= new Stack();
441 parents
.push(new Integer(element_index
));
442 object_dtypes
= new Hashtable();
443 object_gtypes
= new Hashtable();
444 object_etags
= new Hashtable();
445 object_statuses
= new Hashtable();
446 object_list
= new ArrayList();
449 public void endDocument() throws SAXException
{
452 public void startElement(String uri
, String localName
, String qName
, Attributes atts
) throws SAXException
{
454 int parentNodeIx
= ((Integer
) parents
.peek()).intValue();
455 // this element in turn becomes the parent for subsequent routines
456 Integer key
= new Integer(element_index
);
457 element_names
.put(key
, qName
);
459 charBuffers
.put(key
, new StringBuffer());
463 public void endElement(String uri
, String localName
, String qName
) throws SAXException
{
464 Integer key
= (Integer
) parents
.pop();
465 StringBuffer charBuffer
= (StringBuffer
) charBuffers
.get(key
);
466 if (uri
.contains("groupdav.org")) { // GroupDAV namespace
467 if (localName
.contains("vevent-collection") ||
468 localName
.contains("vtodo-collection") ||
469 localName
.contains("vcard-collection")) {
470 obj_gtype
= localName
;
472 } else if (uri
.contains("DAV:")) { // DAV namespace
473 if (localName
.contains("collection")) {
474 obj_dtype
= localName
;
475 } else if (localName
.contains("status")) {
476 obj_status
= charBuffer
.toString();
477 } else if (localName
.contains("href")) {
478 obj_href
= charBuffer
.toString();
479 } else if (localName
.contains("getetag")) {
480 obj_etag
= charBuffer
.toString();
483 if (uri
.contains("DAV:") && localName
.contains("response")) {
484 // Stuff all the known data about the object into the collections
485 object_list
.add(obj_href
);
486 object_etags
.put(obj_href
, obj_etag
);
487 object_statuses
.put(obj_href
, obj_status
);
488 object_dtypes
.put(obj_href
, obj_dtype
);
489 object_gtypes
.put(obj_href
, obj_gtype
);
491 /* Reset ready for the next one */
492 if (localName
.contains("response")) {
501 public void characters(char[] ch
, int start
, int length
) throws SAXException
{
502 String chars
= new String(ch
, start
, length
);
503 if (chars
.trim().length() > 0) {
504 Integer parent
= (Integer
) parents
.peek();
505 StringBuffer charBuffer
= (StringBuffer
) charBuffers
.get(parent
);
506 charBuffer
.append(chars
);
510 public ArrayList
getObjectList() {
514 public Hashtable
getObjectEtags() {
518 public Hashtable
getObjectStatuses() {
519 return object_statuses
;
522 public Hashtable
getObjectDtypes() {
523 return object_dtypes
;
526 public Hashtable
getObjectGtypes() {
527 return object_gtypes
;
531 /** Create a socket to the host */
532 private Socket
createSocket() throws Exception
{
535 st
= ssf
.createSocket(host
, po
);
537 st
= new Socket(host
, po
);
542 private String
readChars() throws Exception
{
543 th
.sleep(sock
.getSoTimeout());
544 String chars
= sbuf
.toString();
546 sbuf
= new StringBuffer();
550 private int findContentLength(String data
) {
551 String
[] headers
= data
.split("\n");
552 for (int i
= 0; i
< headers
.length
; i
++) {
553 String curHeader
= headers
[i
];
554 String
[] line
= curHeader
.split(":");
555 if (line
[0].equalsIgnoreCase("Content-Length")) {
556 Integer it
= new Integer(line
[1].trim());
557 return it
.intValue();
562 /** Sends a request to the server, non-keepalive
564 * @param by A Byte array containing the HTTP request headers and contents
565 * @return A String of the server result
566 * @throws java.lang.Exception Due to network error
568 private String
sendNonKeepAliveRequest(byte[] by
) throws Exception
{
569 String dbugstring
= new String(by
);
570 logger
.fine(dbugstring
);
571 String loggableString
= dbugstring
.replace(base64cache
, "Authorization: Basic [removed]");
572 logger
.finer("We sent:\r\n" + loggableString
);
574 long time
= new java
.util
.Date().getTime();
575 Socket st
= createSocket();
576 st
.getOutputStream().write(by
);
577 st
.getOutputStream().flush();
578 InputStream sc
= st
.getInputStream();
579 BufferedInputStream bis
= new BufferedInputStream(sc
);
581 StringBuffer sb
= new StringBuffer();
585 // We need to process the message character by chacter instead of byte by byte
586 // or otherwise 2-4 byte characters (many Unicode characters) get stripped away.
587 // To do this, we use the InputStreamReader class to convert byte stream into
588 // an Unicode character stream.
589 BufferedReader charstream
= new BufferedReader(new InputStreamReader(bis
, "UTF-8"));
592 char[] buf
= new char[300];
593 op
= charstream
.read(buf
);
594 if (op
!= 0 && op
!= -1) {
595 for (int i
= 0; i
< op
; i
++) {
597 // Strip bytes which would foul an XML parser, i.e SIF
598 // for Funambol upstream. See
599 // http://cse-mjmcl.cse.bris.ac.uk/blog/2007/02/14/1171465494443.html
603 ((c
>= 0x20) && (c
<= 0xD7FF)) ||
604 ((c
>= 0xE000) && (c
<= 0xFFFD)) ||
605 ((c
>= 0x10000) && (c
<= 0x10FFFF))) {
615 logger
.fine(fouls
+ " invalid (XML-wise) characters detected in stream. Is the source/users clean?");
619 long closetime
= new java
.util
.Date().getTime();
620 long rtt
= closetime
- time
;
621 logger
.finer("We got:\r\n" + sb
.toString() + "\r\n....in " + rtt
+ "ms");
622 return sb
.toString();
625 /** Returns a list of HTTP/DAV etags. Object URL's are the key in the Map.
626 * If the DAV PROPFIND did not return etag properties in its result
627 * an attempt to get() a etag will return null as per Map implemementation rules
629 public Map
getEtags() {
630 return dirFinderHandler
.getObjectEtags();
633 private byte[] buildGroupDAVQuery(String method
, String addr
, String contents
,
635 String query
= method
+ " " + addr
+ " HTTP/1.1";
637 headers
.length
; i
++) {
638 query
+= "\n" + headers
[i
];
641 if (contents
!= null) {
642 query
+= "\nContent-Length: " + contents
.getBytes().length
;
644 query
+= "\nContent-Length: 0";
647 query
+= "\nUser-Agent: " + USER_AGENT
+ "\nHost: " + host
+
649 // "Host: " + host + "\n\n\n";
650 if (contents
!= null) {
656 return query
.getBytes();