package davmail.caldav; import davmail.AbstractConnection; import davmail.Settings; import davmail.exchange.ExchangeSession; import davmail.exchange.ExchangeSessionFactory; import davmail.exchange.ICSBufferedReader; import davmail.tray.DavGatewayTray; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.auth.AuthenticationException; import org.apache.commons.httpclient.util.URIUtil; import org.apache.log4j.Logger; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.io.IOException; import java.io.StringReader; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.text.SimpleDateFormat; import java.util.*; /** * Handle a caldav connection. */ public class CaldavConnection extends AbstractConnection { protected Logger wireLogger = Logger.getLogger(this.getClass()); protected boolean closed = false; // Initialize the streams and start the thread public CaldavConnection(Socket clientSocket) { super("CaldavConnection", clientSocket, "UTF-8"); wireLogger.setLevel(Settings.getLoggingLevel("httpclient.wire")); } protected Map parseHeaders() throws IOException { HashMap headers = new HashMap(); String line; while ((line = readClient()) != null && line.length() > 0) { int index = line.indexOf(':'); if (index <= 0) { throw new IOException("Invalid header: " + line); } headers.put(line.substring(0, index).toLowerCase(), line.substring(index + 1).trim()); } return headers; } protected String getContent(String contentLength) throws IOException { if (contentLength == null || contentLength.length() == 0) { return null; } else { int size; try { size = Integer.parseInt(contentLength); } catch (NumberFormatException e) { throw new IOException("Invalid content length: " + contentLength); } char[] buffer = new char[size]; StringBuilder builder = new StringBuilder(); int actualSize = in.read(buffer); builder.append(buffer, 0, actualSize); if (actualSize < 0) { throw new IOException("End of stream reached reading content"); } // dirty hack to ensure full content read // TODO : replace with a dedicated reader while (builder.toString().getBytes("UTF-8").length < size) { actualSize = in.read(buffer); builder.append(buffer, 0, actualSize); } return builder.toString(); } } protected void setSocketTimeout(String keepAliveValue) throws IOException { if (keepAliveValue != null && keepAliveValue.length() > 0) { int keepAlive; try { keepAlive = Integer.parseInt(keepAliveValue); } catch (NumberFormatException e) { throw new IOException("Invalid Keep-Alive: " + keepAliveValue); } if (keepAlive > 300) { keepAlive = 300; } client.setSoTimeout(keepAlive * 1000); DavGatewayTray.debug("Set socket timeout to " + keepAlive + " seconds"); } } public void run() { String line; StringTokenizer tokens; try { while (!closed) { line = readClient(); // unable to read line, connection closed ? if (line == null) { break; } tokens = new StringTokenizer(line); if (tokens.hasMoreTokens()) { String command = tokens.nextToken(); Map headers = parseHeaders(); if (tokens.hasMoreTokens()) { String path = tokens.nextToken(); String content = getContent(headers.get("content-length")); setSocketTimeout(headers.get("keep-alive")); // client requested connection close closed = "close".equals(headers.get("connection")); if ("OPTIONS".equals(command)) { sendOptions(); } else if (!headers.containsKey("authorization")) { sendUnauthorized(); } else { decodeCredentials(headers.get("authorization")); // authenticate only once if (session == null) { // first check network connectivity ExchangeSessionFactory.checkConfig(); try { session = ExchangeSessionFactory.getInstance(userName, password); } catch (AuthenticationException e) { sendUnauthorized(); } } if (session != null) { handleRequest(command, path, headers, content); } } } else { sendErr(HttpStatus.SC_NOT_IMPLEMENTED, "Invalid URI"); } } os.flush(); } } catch (SocketTimeoutException e) { DavGatewayTray.debug("Closing connection on timeout"); } catch (SocketException e) { DavGatewayTray.debug("Connection closed"); } catch (IOException e) { DavGatewayTray.error(e); try { sendErr(HttpStatus.SC_INTERNAL_SERVER_ERROR, e); } catch (IOException e2) { DavGatewayTray.debug("Exception sending error to client", e2); } } finally { close(); } DavGatewayTray.resetIcon(); } protected int getDepth(Map headers) { int result = 0; String depthValue = headers.get("depth"); if (depthValue != null) { try { result = Integer.valueOf(depthValue); } catch (NumberFormatException e) { DavGatewayTray.warn("Invalid depth value: " + depthValue); } } return result; } public void handleRequest(String command, String path, Map headers, String body) throws IOException { int depth = getDepth(headers); String[] paths = path.split("/"); // full debug trace if (wireLogger.isDebugEnabled()) { wireLogger.debug("Caldav command: " + command + " " + path + " depth: " + depth + "\n" + body); } CaldavRequest request = null; if ("PROPFIND".equals(command) || "REPORT".equals(command)) { request = new CaldavRequest(body); } if ("OPTIONS".equals(command)) { sendOptions(); // redirect PROPFIND on / to current user principal } else if ("PROPFIND".equals(command) && (paths.length == 0 || paths.length == 1)) { sendRoot(request); } else if ("GET".equals(command) && (paths.length == 0 || paths.length == 1)) { sendGetRoot(); // return current user calendar } else if ("calendar".equals(paths[1])) { StringBuilder message = new StringBuilder(); message.append("/calendar no longer supported, recreate calendar with /users/") .append(session.getEmail()).append("/calendar"); DavGatewayTray.error(message.toString()); sendErr(HttpStatus.SC_BAD_REQUEST, message.toString()); } else if ("user".equals(paths[1])) { sendRedirect(headers, "/principals/users/" + session.getEmail()); // principal namespace } else if ("PROPFIND".equals(command) && "principals".equals(paths[1]) && paths.length == 4 && "users".equals(paths[2])) { sendPrincipal(request, paths[3]); // send back principal on search } else if ("REPORT".equals(command) && "principals".equals(paths[1]) && paths.length == 3 && "users".equals(paths[2])) { sendPrincipal(request, session.getEmail()); // user root } else if ("PROPFIND".equals(command) && "users".equals(paths[1]) && paths.length == 3) { sendUserRoot(request, depth, paths[2]); } else if ("PROPFIND".equals(command) && "users".equals(paths[1]) && paths.length == 4 && "inbox".equals(paths[3])) { sendInbox(request, paths[2]); } else if ("REPORT".equals(command) && "users".equals(paths[1]) && paths.length == 4 && "inbox".equals(paths[3])) { reportInbox(); } else if ("PROPFIND".equals(command) && "users".equals(paths[1]) && paths.length == 4 && "outbox".equals(paths[3])) { sendOutbox(request, paths[2]); } else if ("POST".equals(command) && "users".equals(paths[1]) && paths.length == 4 && "outbox".equals(paths[3])) { if (body.indexOf("VFREEBUSY") >= 0) { sendFreeBusy(body); } else { int status = session.sendEvent(body); sendHttpResponse(status, true); } } else if ("PROPFIND".equals(command) && "users".equals(paths[1]) && paths.length == 4 && "calendar".equals(paths[3])) { sendCalendar(request, depth, paths[2]); } else if ("REPORT".equals(command) && "users".equals(paths[1]) && paths.length == 4 && "calendar".equals(paths[3]) // only current user for now && session.getEmail().equalsIgnoreCase(paths[2])) { reportCalendar(request); } else if ("PUT".equals(command) && "users".equals(paths[1]) && paths.length == 5 && "calendar".equals(paths[3]) // only current user for now && session.getEmail().equalsIgnoreCase(paths[2])) { String etag = headers.get("if-match"); String noneMatch = headers.get("if-none-match"); ExchangeSession.EventResult eventResult = session.createOrUpdateEvent(paths[4], body, etag, noneMatch); if (eventResult.etag != null) { HashMap responseHeaders = new HashMap(); responseHeaders.put("GetETag", eventResult.etag); sendHttpResponse(eventResult.status, responseHeaders, "text/html", "", true); } else { sendHttpResponse(eventResult.status, true); } } else if ("DELETE".equals(command) && "users".equals(paths[1]) && paths.length == 5 && "calendar".equals(paths[3]) // only current user for now && session.getEmail().equalsIgnoreCase(paths[2])) { int status = session.deleteEvent(paths[4]); sendHttpResponse(status, true); } else if ("GET".equals(command) && "users".equals(paths[1]) && paths.length == 5 && "calendar".equals(paths[3]) // only current user for now && session.getEmail().equalsIgnoreCase(paths[2])) { ExchangeSession.Event event = session.getEvent(paths[4]); sendHttpResponse(HttpStatus.SC_OK, null, "text/calendar;charset=UTF-8", event.getICS(), true); } else { StringBuilder message = new StringBuilder(); message.append("Unsupported request: ").append(command).append(" ").append(path); message.append(" Depth: ").append(depth).append("\n").append(body); DavGatewayTray.error(message.toString()); sendErr(HttpStatus.SC_BAD_REQUEST, message.toString()); } } protected void appendEventsResponses(StringBuilder buffer, CaldavRequest request, List events) throws IOException { int size = events.size(); int count = 0; for (ExchangeSession.Event event : events) { DavGatewayTray.debug("Retrieving event " + (++count) + "/" + size); appendEventResponse(buffer, request, event); } } protected void appendEventResponse(StringBuilder buffer, CaldavRequest request, ExchangeSession.Event event) throws IOException { String eventPath = event.getPath().replaceAll("<", "<").replaceAll(">", ">"); buffer.append(""); buffer.append("/users/").append(session.getEmail()).append("/calendar").append(URIUtil.encodeWithinQuery(eventPath)).append(""); buffer.append(""); buffer.append(""); if (request.hasProperty("calendar-data")) { String ics = event.getICS(); if (ics != null && ics.length() > 0) { ics = ics.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); buffer.append(""); buffer.append(ics); buffer.append(""); } } if (request.hasProperty("getetag")) { buffer.append("").append(event.getEtag()).append(""); } if (request.hasProperty("resourcetype")) { buffer.append(""); } if (request.hasProperty("displayname")) { buffer.append("").append(eventPath).append(""); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); } public void appendCalendar(StringBuilder buffer, String principal, CaldavRequest request) throws IOException { buffer.append(""); buffer.append("/users/").append(principal).append("/calendar"); buffer.append(""); buffer.append(""); if (request.hasProperty("resourcetype")) { buffer.append(""); buffer.append(""); buffer.append(""); buffer.append(""); } if (request.hasProperty("owner")) { buffer.append(""); buffer.append("/principals/users/").append(principal).append(""); buffer.append(""); } if (request.hasProperty("getetag")) { buffer.append("") .append(session.getCalendarEtag()) .append(""); } if (request.hasProperty("getctag")) { buffer.append("") .append(base64Encode(session.getCalendarCtag())) .append(""); } if (request.hasProperty("displayname")) { buffer.append("calendar"); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); } public void appendInbox(StringBuilder buffer, String principal, CaldavRequest request) throws IOException { buffer.append(""); buffer.append("/users/").append(principal).append("/inbox"); buffer.append(""); buffer.append(""); if (request.hasProperty("resourcetype")) { buffer.append(""); buffer.append(""); buffer.append(""); buffer.append(""); } if (request.hasProperty("getctag")) { buffer.append("0"); } if (request.hasProperty("displayname")) { buffer.append("inbox"); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); } public void appendOutbox(StringBuilder buffer, String principal, CaldavRequest request) throws IOException { buffer.append(""); buffer.append("/users/").append(principal).append("/outbox"); buffer.append(""); buffer.append(""); if (request.hasProperty("resourcetype")) { buffer.append(""); buffer.append(""); buffer.append(""); buffer.append(""); } if (request.hasProperty("getctag")) { buffer.append("0"); } if (request.hasProperty("displayname")) { buffer.append("outbox"); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); } public void sendGetRoot() throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append("Connected to DavMail
"); buffer.append("UserName :").append(userName).append("
"); buffer.append("Email :").append(session.getEmail()).append("
"); sendHttpResponse(HttpStatus.SC_OK, null, "text/html;charset=UTF-8", buffer.toString(), true); } public void sendInbox(CaldavRequest request, String principal) throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append(""); buffer.append(""); appendInbox(buffer, principal, request); buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void reportInbox() throws IOException { // inbox is always empty StringBuilder buffer = new StringBuilder(); buffer.append("" + ""); buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void sendOutbox(CaldavRequest request, String principal) throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append(""); buffer.append(""); appendOutbox(buffer, principal, request); buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void sendCalendar(CaldavRequest request, int depth, String principal) throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append(""); buffer.append(""); appendCalendar(buffer, principal, request); if (depth == 1) { DavGatewayTray.debug("Searching calendar events..."); List events = session.getAllEvents(); DavGatewayTray.debug("Found " + events.size() + " calendar events"); appendEventsResponses(buffer, request, events); } buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } protected String getEventFileNameFromPath(String path) { int index = path.indexOf("/calendar/"); if (index < 0) { return null; } else { return path.substring(index + "/calendar/".length()); } } public void reportCalendar(CaldavRequest request) throws IOException { List events; List notFound = new ArrayList(); if (request.isMultiGet()) { events = new ArrayList(); int count = 0; int total = request.getHrefs().size(); for (String href : request.getHrefs()) { DavGatewayTray.debug("Report event " + (++count) + "/" + total); try { String eventName = getEventFileNameFromPath(href); if (eventName == null) { notFound.add(href); } else { events.add(session.getEvent(eventName)); } } catch (HttpException e) { notFound.add(href); } } } else { events = session.getAllEvents(); } StringBuilder buffer = new StringBuilder(); buffer.append("" + ""); appendEventsResponses(buffer, request, events); // send not found events errors for (String href : notFound) { buffer.append(""); buffer.append("").append(URIUtil.encodeWithinQuery(href)).append(""); buffer.append(""); buffer.append("HTTP/1.1 404 Not Found"); buffer.append(""); buffer.append(""); } buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void sendUserRoot(CaldavRequest request, int depth, String principal) throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append(""); buffer.append(""); buffer.append(""); buffer.append("/users/").append(principal).append(""); buffer.append(""); buffer.append(""); if (request.hasProperty("resourcetype")) { buffer.append(""); buffer.append(""); buffer.append(""); } if (request.hasProperty("displayname")) { buffer.append("").append(principal).append(""); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); if (depth == 1) { appendInbox(buffer, principal, request); appendOutbox(buffer, principal, request); appendCalendar(buffer, principal, request); } buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void sendRoot(CaldavRequest request) throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append(""); buffer.append(""); buffer.append(""); buffer.append("/"); buffer.append(""); buffer.append(""); if (request.hasProperty("principal-collection-set")) { buffer.append(""); buffer.append("/principals/users/").append(session.getEmail()).append(""); buffer.append(""); } if (request.hasProperty("displayname")) { buffer.append("ROOT"); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void sendPrincipal(CaldavRequest request, String principal) throws IOException { StringBuilder buffer = new StringBuilder(); buffer.append(""); buffer.append(""); buffer.append(""); buffer.append("/principals/users/").append(principal).append(""); buffer.append(""); buffer.append(""); if (request.hasProperty("calendar-home-set")) { buffer.append(""); buffer.append("/users/").append(principal).append(""); buffer.append(""); } if (request.hasProperty("calendar-user-address-set")) { buffer.append(""); buffer.append("mailto:").append(principal).append(""); buffer.append(""); } if (request.hasProperty("schedule-inbox-URL")) { buffer.append(""); buffer.append("/users/").append(principal).append("/inbox"); buffer.append(""); } if (request.hasProperty("schedule-outbox-URL")) { buffer.append(""); buffer.append("/users/").append(principal).append("/outbox"); buffer.append(""); } if (request.hasProperty("displayname")) { buffer.append("").append(principal).append(""); } if (request.hasProperty("resourcetype")) { buffer.append(""); buffer.append(""); buffer.append(""); buffer.append(""); } buffer.append(""); buffer.append("HTTP/1.1 200 OK"); buffer.append(""); buffer.append(""); buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } public void sendFreeBusy(String body) throws IOException { HashMap valueMap = new HashMap(); ArrayList attendees = new ArrayList(); HashMap attendeeKeyMap = new HashMap(); ICSBufferedReader reader = new ICSBufferedReader(new StringReader(body)); String line; String key; while ((line = reader.readLine()) != null) { int index = line.indexOf(':'); if (index <= 0) { throw new IOException("Invalid request: " + body); } String fullkey = line.substring(0, index); String value = line.substring(index + 1); int semicolonIndex = fullkey.indexOf(";"); if (semicolonIndex > 0) { key = fullkey.substring(0, semicolonIndex); } else { key = fullkey; } if ("ATTENDEE".equals(key)) { attendees.add(value); attendeeKeyMap.put(value, fullkey); } else { valueMap.put(key, value); } } // get freebusy for each attendee HashMap freeBusyMap = new HashMap(); for (String attendee : attendees) { String freeBusy = session.getFreebusy(attendee, valueMap); if (freeBusy != null) { freeBusyMap.put(attendee, freeBusy); } } if (!freeBusyMap.isEmpty()) { StringBuilder response = new StringBuilder(); response.append("") .append(""); for (Map.Entry entry : freeBusyMap.entrySet()) { String attendee = entry.getKey(); response.append("") .append("") .append("").append(attendee).append("") .append("") .append("2.0;Success") .append("BEGIN:VCALENDAR").append((char) 13).append((char) 10) .append("VERSION:2.0").append((char) 13).append((char) 10) .append("PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN").append((char) 13).append((char) 10) .append("METHOD:REPLY").append((char) 13).append((char) 10) .append("BEGIN:VFREEBUSY").append((char) 13).append((char) 10) .append("DTSTAMP:").append(valueMap.get("DTSTAMP")).append("").append((char) 13).append((char) 10) .append("ORGANIZER:").append(valueMap.get("ORGANIZER")).append("").append((char) 13).append((char) 10) .append("DTSTART:").append(valueMap.get("DTSTART")).append("").append((char) 13).append((char) 10) .append("DTEND:").append(valueMap.get("DTEND")).append("").append((char) 13).append((char) 10) .append("UID:").append(valueMap.get("UID")).append("").append((char) 13).append((char) 10) .append(attendeeKeyMap.get(attendee)).append(":").append(attendee).append("").append((char) 13).append((char) 10); if (entry.getValue().length() > 0) { response.append("FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:").append(entry.getValue()).append("").append((char) 13).append((char) 10); } response.append("END:VFREEBUSY").append((char) 13).append((char) 10) .append("END:VCALENDAR") .append("") .append(""); } response.append(""); sendHttpResponse(HttpStatus.SC_OK, null, "text/xml;charset=UTF-8", response.toString(), true); } else { sendHttpResponse(HttpStatus.SC_NOT_FOUND, null, "text/plain", "Unknown recipient: " + valueMap.get("ATTENDEE"), true); } } public void sendRedirect(Map headers, String path) throws IOException { StringBuilder buffer = new StringBuilder(); if (headers.get("host") != null) { buffer.append("http://").append(headers.get("host")); } buffer.append(path); Map responseHeaders = new HashMap(); responseHeaders.put("Location", buffer.toString()); sendHttpResponse(HttpStatus.SC_MOVED_PERMANENTLY, responseHeaders, null, null, true); } public void sendErr(int status, Exception e) throws IOException { String message = e.getMessage(); if (message == null) { message = e.toString(); } sendErr(status, message); } public void sendErr(int status, String message) throws IOException { sendHttpResponse(status, null, "text/plain;charset=UTF-8", message, false); } public void sendOptions() throws IOException { HashMap headers = new HashMap(); headers.put("Allow", "OPTIONS, GET, PROPFIND, PUT, POST"); sendHttpResponse(HttpStatus.SC_OK, headers, null, null, true); } public void sendUnauthorized() throws IOException { HashMap headers = new HashMap(); headers.put("WWW-Authenticate", "Basic realm=\"" + Settings.getProperty("davmail.url") + "\""); sendHttpResponse(HttpStatus.SC_UNAUTHORIZED, headers, null, null, true); } public void sendHttpResponse(int status, boolean keepAlive) throws IOException { sendHttpResponse(status, null, null, null, keepAlive); } public void sendHttpResponse(int status, Map headers, String contentType, String content, boolean keepAlive) throws IOException { sendClient("HTTP/1.1 " + status + " " + HttpStatus.getStatusText(status)); sendClient("Server: DavMail Gateway"); sendClient("DAV: 1, 2, 3, access-control, calendar-access, ticket, calendar-schedule, calendarserver-private-events"); SimpleDateFormat formatter = new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH); sendClient("Date: " + formatter.format(new java.util.Date())); if (headers != null) { for (Map.Entry header : headers.entrySet()) { sendClient(header.getKey() + ": " + header.getValue()); } } if (contentType != null) { sendClient("Content-Type: " + contentType); } closed = closed || !keepAlive; sendClient("Connection: " + (closed ? "close" : "keep-alive")); if (content != null && content.length() > 0) { sendClient("Content-Length: " + content.getBytes("UTF-8").length); } else { sendClient("Content-Length: 0"); } sendClient(""); if (content != null && content.length() > 0) { // full debug trace if (wireLogger.isDebugEnabled()) { wireLogger.debug("> " + content); } sendClient(content.getBytes("UTF-8")); } } /** * Decode HTTP credentials * * @param authorization http authorization header value * @throws java.io.IOException if invalid credentials */ protected void decodeCredentials(String authorization) throws IOException { int index = authorization.indexOf(' '); if (index > 0) { String mode = authorization.substring(0, index).toLowerCase(); if (!"basic".equals(mode)) { throw new IOException("Unsupported authorization mode: " + mode); } String encodedCredentials = authorization.substring(index + 1); String decodedCredentials = base64Decode(encodedCredentials); index = decodedCredentials.indexOf(':'); if (index > 0) { userName = decodedCredentials.substring(0, index); password = decodedCredentials.substring(index + 1); } else { throw new IOException("Invalid credentials"); } } else { throw new IOException("Invalid credentials"); } } protected static class CaldavRequest { protected HashSet properties = new HashSet(); protected HashSet hrefs; protected boolean isMultiGet; public CaldavRequest(String body) throws IOException { // parse body XMLStreamReader streamReader = null; try { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); streamReader = inputFactory.createXMLStreamReader(new StringReader(body)); boolean inElement = false; boolean inProperties = false; String currentElement = null; while (streamReader.hasNext()) { int event = streamReader.next(); if (event == XMLStreamConstants.START_ELEMENT) { inElement = true; currentElement = streamReader.getLocalName(); if ("prop".equals(currentElement)) { inProperties = true; } else if ("calendar-multiget".equals(currentElement)) { isMultiGet = true; } else if (inProperties) { properties.add(currentElement); } } else if (event == XMLStreamConstants.END_ELEMENT) { if ("prop".equals(currentElement)) { inProperties = false; } } else if (event == XMLStreamConstants.CHARACTERS && inElement) { if ("href".equals(currentElement)) { if (hrefs == null) { hrefs = new HashSet(); } hrefs.add(URIUtil.decode(streamReader.getText())); } inElement = false; } } } catch (XMLStreamException e) { throw new IOException(e.getMessage()); } finally { try { if (streamReader != null) { streamReader.close(); } } catch (XMLStreamException e) { DavGatewayTray.error(e); } } } public boolean hasProperty(String propertyName) { return properties.contains(propertyName); } public boolean isMultiGet() { return isMultiGet && hrefs != null; } public Set getHrefs() { return hrefs; } } }