Mega experimental fixes; try to stop a new smallsql connection from being opened...
[jgroupdav.git] / src / main / java / net / bionicmessage / groupdav / groupDAV.java
blobee62c8c2d6514e5e704e4da47d7ddd503789c232
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 java.nio.ByteBuffer;
30 import net.bionicmessage.extutils.*;
31 import java.util.*;
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;
41 /**
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 * &quot;Autorization: Basic pass:user&quot;
76 * (Google HTTP Basic authentication for more information)
78 public groupDAV(String url, String b64cache) {
79 origurl = url;
80 try {
81 serverURL = new URL(url);
82 if (serverURL.getProtocol().contains("https")) {
83 ssl = true;
85 tok = "https://";
86 host = serverURL.getHost();
87 po = serverURL.getPort();
88 if (po == -1) {
89 po = 80;
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("/")) {
96 origurl = url + "/";
98 if (url.indexOf("https://") != -1) {
99 ssl = true;
100 tok = "https://";
102 base64cache = "Authorization: Basic " + b64cache;
103 init();
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) {
113 origurl = url;
114 try {
115 serverURL = new URL(url);
116 if (serverURL.getProtocol().contains("https")) {
117 ssl = true;
118 tok = "https://";
120 host = serverURL.getHost();
121 po = serverURL.getPort();
122 if (po == -1) {
123 po = 80;
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("/")) {
130 origurl = url + "/";
132 if (url.indexOf("https://") != -1) {
133 ssl = true;
134 tok = "https://";
136 base64cache = "Authorization: Basic " + Base64.encodeBytes(new String(user + ":" + pass).getBytes());
137 init();
140 /** Set the logger to output to
141 * @param log The logger instance to use
143 public void setLogger(Logger log) {
144 logger = log;
147 /** Initiate client */
148 public void init() {
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();
157 try {
158 sbuf = new StringBuffer();
159 // sock = createSocket();
160 //sock.setKeepAlive(true);
161 // setupReadThread();
162 // th.setDaemon(true);
163 // th.start();
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);
176 if (clength == -1) {
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);
186 return false;
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);
208 if (gtype != null) {
209 locations.put(url, gtype);
213 return locations;
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);
222 if (clength == -1) {
223 logger.warning("We don't have a Content Length field in listObjects");
224 /* Temporary, ugly workaround for chunked transfer. Only use if
225 * you need to
226 if (output.endsWith("0")) {
227 int startOfXML = output.indexOf("<?xml");
228 output = output.substring(startOfXML, output.lastIndexOf("0"));
229 } */
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 */
244 } else {
245 cleaned.add(lurl);
248 return cleaned;
251 /** Posts an object */
252 public GroupDAVObject postObject(String subdir, String uid, String contents) throws Exception {
253 uid = uid.trim();
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",
260 "If-None-Match: *",
261 base64cache
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());
284 return gobjG;
285 } else {
286 return gobjP;
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,
297 String contents,
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
321 * what we gave it */
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());
332 return gobjG;
333 } else {
334 return gobjP;
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);
345 return gbo;
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);
361 return obj;
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) {
372 gdir = "";
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" +
382 XMLSTRING;
383 return PROPFIND.getBytes();
386 /* public byte[] buildPROPFINDList(String subdir) {
387 String gdir = sdir + "/";
388 if (subdir.indexOf(sdir) > -1) {
389 gdir = "";
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" +
400 XMLSTRING;
401 return PROPFIND.getBytes();
403 } */
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;
419 Logger lg;
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) {
433 lg = l;
436 public void startDocument() throws SAXException {
437 charBuffers = new Hashtable();
438 element_names = new Hashtable();
439 element_index = -1;
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 {
453 ++element_index;
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);
458 parents.push(key);
459 charBuffers.put(key, new StringBuffer());
460 // etc ...
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")) {
493 obj_href = "";
494 obj_dtype = "";
495 obj_gtype = "";
496 obj_status = "";
497 obj_etag = "";
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() {
511 return object_list;
514 public Hashtable getObjectEtags() {
515 return object_etags;
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 {
533 Socket st = null;
534 if (ssl) {
535 st = ssf.createSocket(host, po);
536 } else {
537 st = new Socket(host, po);
539 return st;
542 private String readChars() throws Exception {
543 th.sleep(sock.getSoTimeout());
544 String chars = sbuf.toString();
545 // clear buffer
546 sbuf = new StringBuffer();
547 return chars;
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();
560 return -1;
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);
573 // Measure latency
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();
582 int op = 0;
583 int fouls = 0;
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"));
591 while (op != -1) {
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++) {
596 char c = buf[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
600 if ((c == 0x9) ||
601 (c == 0xA) ||
602 (c == 0xD) ||
603 ((c >= 0x20) && (c <= 0xD7FF)) ||
604 ((c >= 0xE000) && (c <= 0xFFFD)) ||
605 ((c >= 0x10000) && (c <= 0x10FFFF))) {
606 sb.append(c);
607 } else {
608 fouls++;
614 if (fouls > 0) {
615 logger.fine(fouls + " invalid (XML-wise) characters detected in stream. Is the source/users clean?");
617 charstream.close();
618 bis.close();
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,
634 String[] headers) {
635 String query = method + " " + addr + " HTTP/1.1";
636 for (int i = 0; i <
637 headers.length; i++) {
638 query += "\n" + headers[i];
641 if (contents != null) {
642 query += "\nContent-Length: " + contents.getBytes().length;
643 } else {
644 query += "\nContent-Length: 0";
647 query += "\nUser-Agent: " + USER_AGENT + "\nHost: " + host +
648 "\n\n";
649 // "Host: " + host + "\n\n\n";
650 if (contents != null) {
651 query += contents;
652 } else {
653 query += "\n\n";
656 return query.getBytes();