org.exoplatform.calendar.service.impl.RemoteCalendarServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.exoplatform.calendar.service.impl.RemoteCalendarServiceImpl.java

Source

/*
 * Copyright (C) 2003-2011 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.calendar.service.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.Map.Entry;

import javax.jcr.ItemExistsException;
import javax.jcr.Node;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.NumberList;
import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.Recur;
import net.fortuna.ical4j.model.WeekDay;
import net.fortuna.ical4j.model.WeekDayList;
import net.fortuna.ical4j.model.component.VAlarm;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VFreeBusy;
import net.fortuna.ical4j.model.component.VToDo;
import net.fortuna.ical4j.model.property.Attach;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.Clazz;
import net.fortuna.ical4j.model.property.ExDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.RecurrenceId;
import net.fortuna.ical4j.util.CompatibilityHints;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.OptionsMethod;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.webdav.DavConstants;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.MultiStatus;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.client.methods.ReportMethod;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.apache.jackrabbit.webdav.version.report.ReportInfo;
import org.apache.jackrabbit.webdav.xml.DomUtil;
import org.apache.jackrabbit.webdav.xml.Namespace;
import org.exoplatform.calendar.service.Attachment;
import org.exoplatform.calendar.service.Calendar;
import org.exoplatform.calendar.service.CalendarEvent;
import org.exoplatform.calendar.service.CalendarService;
import org.exoplatform.calendar.service.EventCategory;
import org.exoplatform.calendar.service.EventQuery;
import org.exoplatform.calendar.service.RemoteCalendar;
import org.exoplatform.calendar.service.RemoteCalendarService;
import org.exoplatform.calendar.service.Utils;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Created by The eXo Platform SAS
 * Author : eXoPlatform
 * exo@exoplatform.com
 * Jan 10, 2011
 */
public class RemoteCalendarServiceImpl implements RemoteCalendarService {

    private static final Namespace CALDAV_NAMESPACE = Namespace.getNamespace("C", "urn:ietf:params:xml:ns:caldav");

    private static final String CALDAV_XML_CALENDAR_MULTIGET = "calendar-multiget";

    private static final String CALDAV_XML_CALENDAR_QUERY = "calendar-query";

    private static final String CALDAV_XML_CALENDAR_DATA = "calendar-data";

    private static final String CALDAV_XML_FILTER = "filter";

    private static final String CALDAV_XML_COMP_FILTER = "comp-filter";

    private static final String CALDAV_XML_TIME_RANGE = "time-range";

    private static final String CALDAV_XML_START = "start";

    private static final String CALDAV_XML_END = "end";

    private static final String CALDAV_XML_COMP_FILTER_NAME = "name";

    private static final Log logger = ExoLogger.getLogger("cs.calendar.service.remote");

    private JCRDataStorage storage_;

    public RemoteCalendarServiceImpl(JCRDataStorage storage) {
        this.storage_ = storage;
    }

    @Override
    public InputStream connectToRemoteServer(RemoteCalendar remoteCalendar) throws Exception {
        HttpClient client = getRemoteClient(remoteCalendar);
        GetMethod get = new GetMethod(remoteCalendar.getRemoteUrl());
        try {
            client.executeMethod(get);
            InputStream icalInputStream = get.getResponseBodyAsStream();
            return icalInputStream;
        } catch (IOException e) {
            logger.debug(e.getMessage());
            throw new IOException(e.getMessage());
        }
    }

    @Override
    public boolean isValidRemoteUrl(String url, String type, String remoteUser, String remotePassword)
            throws IOException, UnsupportedOperationException {
        try {
            HttpClient client = new HttpClient();
            HostConfiguration hostConfig = new HostConfiguration();
            String host = new URL(url).getHost();
            if (StringUtils.isEmpty(host))
                host = url;
            hostConfig.setHost(host);
            client.setHostConfiguration(hostConfig);
            Credentials credentials = null;
            client.setHostConfiguration(hostConfig);
            if (!StringUtils.isEmpty(remoteUser)) {
                credentials = new UsernamePasswordCredentials(remoteUser, remotePassword);
                client.getState().setCredentials(new AuthScope(host, AuthScope.ANY_PORT, AuthScope.ANY_REALM),
                        credentials);
            }

            if (CalendarService.ICALENDAR.equals(type)) {
                GetMethod get = new GetMethod(url);
                client.executeMethod(get);
                int statusCode = get.getStatusCode();
                get.releaseConnection();
                return (statusCode == HttpURLConnection.HTTP_OK);
            } else {
                if (CalendarService.CALDAV.equals(type)) {
                    OptionsMethod options = new OptionsMethod(url);
                    client.executeMethod(options);
                    Header header = options.getResponseHeader("DAV");
                    options.releaseConnection();
                    if (header == null) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Cannot connect to remoter server or not support WebDav access");
                        }
                        return false;
                    }
                    Boolean support = header.toString().contains("calendar-access");
                    options.releaseConnection();
                    if (!support) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Remote server does not support CalDav access");
                        }
                        throw new UnsupportedOperationException("Remote server does not support CalDav access");
                    }
                    return support;
                }
                return false;
            }
        } catch (MalformedURLException e) {
            if (logger.isDebugEnabled())
                logger.debug(e.getMessage(), e);
            throw new IOException("URL is invalid. Maybe no legal protocol or URl could not be parsed");
        } catch (IOException e) {
            if (logger.isDebugEnabled())
                logger.debug(e.getMessage(), e);
            throw new IOException("Error occurs when connecting to remote server");
        }
    }

    @Override
    public Calendar importRemoteCalendar(RemoteCalendar remoteCalendar) throws Exception {
        Calendar eXoCalendar = storage_.createRemoteCalendar(remoteCalendar);
        if (CalendarService.ICALENDAR.equals(remoteCalendar.getType())) {
            remoteCalendar.setCalendarId(eXoCalendar.getId());
            remoteCalendar.setLastUpdated(Utils.getGreenwichMeanTime());
            InputStream icalInputStream = connectToRemoteServer(remoteCalendar);
            CalendarService calService = (CalendarService) ExoContainerContext.getCurrentContainer()
                    .getComponentInstanceOfType(CalendarService.class);
            calService.getCalendarImportExports(CalendarService.ICALENDAR).importCalendar(
                    remoteCalendar.getUsername(), icalInputStream, remoteCalendar.getCalendarId(), null,
                    remoteCalendar.getBeforeTime(), remoteCalendar.getAfterTime(), false);
            return eXoCalendar;
        } else {
            if (CalendarService.CALDAV.equals(remoteCalendar.getType())) {
                MultiStatus multiStatus = connectToCalDavServer(remoteCalendar);
                String href;
                for (int i = 0; i < multiStatus.getResponses().length; i++) {
                    MultiStatusResponse multiRes = multiStatus.getResponses()[i];
                    href = multiRes.getHref();
                    DavPropertySet propSet = multiRes.getProperties(DavServletResponse.SC_OK);
                    net.fortuna.ical4j.model.Calendar iCalEvent = getCalDavResource(remoteCalendar, href);
                    DavProperty etag = propSet.get(DavPropertyName.GETETAG.getName(), DavConstants.NAMESPACE);
                    try {
                        importCaldavEvent(remoteCalendar.getUsername(), eXoCalendar.getId(), null, iCalEvent, href,
                                etag.getValue().toString(), true);
                        storage_.setRemoteCalendarLastUpdated(remoteCalendar.getUsername(), eXoCalendar.getId(),
                                Utils.getGreenwichMeanTime());
                    } catch (Exception e) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Exception occurs when import calendar component " + href
                                    + ". Skip this component.", e);
                        }
                        continue;
                    }
                }
                return eXoCalendar;
            }
            return null;
        }
    }

    @Override
    public Calendar refreshRemoteCalendar(String username, String remoteCalendarId) throws Exception {
        if (!storage_.isRemoteCalendar(username, remoteCalendarId)) {
            if (logger.isDebugEnabled()) {
                logger.debug("This calendar is not remote calendar.");
            }
            return null;
        }
        RemoteCalendar remoteCalendar = storage_.getRemoteCalendar(username, remoteCalendarId);
        if (CalendarService.ICALENDAR.equals(remoteCalendar.getType())) {
            // remove all components in local calendar
            List<String> calendarIds = new ArrayList<String>();
            calendarIds.add(remoteCalendarId);
            EventQuery eventQuery = new EventQuery();
            eventQuery.setCalendarId(new String[] { remoteCalendarId });
            eventQuery.setFromDate(remoteCalendar.getBeforeTime());
            eventQuery.setToDate(remoteCalendar.getAfterTime());
            List<CalendarEvent> events = storage_.getUserEvents(username, eventQuery);
            if (events != null && events.size() > 0) {
                for (CalendarEvent event : events) {
                    if (Utils.isExceptionOccurrence(event))
                        continue;
                    else if (Utils.isRepeatEvent(event)) {
                        storage_.removeRecurrenceSeries(username, event);
                    } else
                        storage_.removeUserEvent(username, remoteCalendarId, event.getId());
                }
            }

            Calendar eXoCalendar = storage_.getUserCalendar(username, remoteCalendarId);
            InputStream icalInputStream = connectToRemoteServer(remoteCalendar);
            CalendarService calService = (CalendarService) ExoContainerContext.getCurrentContainer()
                    .getComponentInstanceOfType(CalendarService.class);
            calService.getCalendarImportExports(CalendarService.ICALENDAR).importCalendar(username, icalInputStream,
                    remoteCalendarId, null, remoteCalendar.getBeforeTime(), remoteCalendar.getAfterTime(), false);
            storage_.setRemoteCalendarLastUpdated(username, eXoCalendar.getId(), Utils.getGreenwichMeanTime());
            return eXoCalendar;
        }

        if (CalendarService.CALDAV.equals(remoteCalendar.getType())) {
            Calendar eXoCalendar = synchronizeWithCalDavServer(remoteCalendar);
            storage_.setRemoteCalendarLastUpdated(username, eXoCalendar.getId(), Utils.getGreenwichMeanTime());
            return eXoCalendar;
        }

        return null;
    }

    /**
     * First time connect to CalDav server to get data
     * 
     * @param remoteCalendar
     * @return
     * @throws Exception
     */
    public MultiStatus connectToCalDavServer(RemoteCalendar remoteCalendar) throws Exception {
        HttpClient client = getRemoteClient(remoteCalendar);
        return doCalendarQuery(client, remoteCalendar.getRemoteUrl(), remoteCalendar.getBeforeTime(),
                remoteCalendar.getAfterTime());
    }

    public net.fortuna.ical4j.model.Calendar getCalDavResource(RemoteCalendar remoteCalendar, String href)
            throws Exception {
        HttpClient client = getRemoteClient(remoteCalendar);
        CalendarBuilder builder = new CalendarBuilder();
        // Enable relaxed-unfolding to allow ical4j parses "folding" line follows iCalendar specification
        CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);

        try {
            MultiStatus multiStatus = doCalendarMultiGet(client, remoteCalendar.getRemoteUrl(),
                    new String[] { href });
            if (multiStatus == null)
                return null;
            MultiStatusResponse multiRes = multiStatus.getResponses()[0];
            DavPropertySet propSet = multiRes.getProperties(DavServletResponse.SC_OK);
            DavProperty calendarData = propSet.get(CALDAV_XML_CALENDAR_DATA, CALDAV_NAMESPACE);
            return builder.build(new StringReader(calendarData.getValue().toString()));
        } catch (Exception e) {
            if (logger.isDebugEnabled()) {
                logger.debug("Can't get resource from CalDav server", e);
            }
            return null;
        }
    }

    /**
     * Get a map of pairs (href,etag) from caldav server
     * This calendar query doesn't include calendar-data element to get data faster
     * 
     * @param client
     * @param uri
     * @param from
     * @param to
     * @return
     * @throws Exception
     */
    public Map<String, String> getEntityTags(HttpClient client, String uri, java.util.Calendar from,
            java.util.Calendar to) throws Exception {
        Map<String, String> etags = new HashMap<String, String>();
        ReportMethod report = makeCalDavQueryReport(uri, from, to);
        if (report == null)
            return null;

        try {
            client.executeMethod(report);
            MultiStatus multiStatus = report.getResponseBodyAsMultiStatus();

            String href;
            for (int i = 0; i < multiStatus.getResponses().length; i++) {
                MultiStatusResponse multiRes = multiStatus.getResponses()[i];
                href = multiRes.getHref();
                DavPropertySet propSet = multiRes.getProperties(DavServletResponse.SC_OK);
                DavProperty etag = propSet.get(DavPropertyName.GETETAG.getName(), DavConstants.NAMESPACE);
                etags.put(href, etag.getValue().toString());
            }

            return etags;
        } catch (Exception e) {
            if (logger.isDebugEnabled())
                logger.debug("Exception occurs when querying entity tags from CalDav server", e);
            return null;
        } finally {
            if (report != null) {
                report.releaseConnection();
            }
        }
    }

    /**
     * Do reload data from CalDav server for remote calendar with a time-range condition.
     * This function first gets entity tag map from server, then compare with data from local
     * to determines which events/task (or other components) need to be update, create or delete
     * 
     * @param remoteCalendar
     * @return Calendar
     * @throws Exception
     */

    public Calendar synchronizeWithCalDavServer(RemoteCalendar remoteCalendar) throws Exception {
        String username = remoteCalendar.getUsername();
        String remoteCalendarId = remoteCalendar.getCalendarId();

        if (!storage_.isRemoteCalendar(username, remoteCalendarId)) {
            return null;
        }
        if (!CalendarService.CALDAV.equals(remoteCalendar.getType())) {
            throw new UnsupportedOperationException("Not support");
        }

        CalendarService calService = (CalendarService) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(CalendarService.class);
        if (calService == null) {
            calService = (CalendarService) ExoContainerContext
                    .getContainerByName(PortalContainer.getCurrentPortalContainerName())
                    .getComponentInstanceOfType(CalendarService.class);
        }
        CalendarBuilder calendarBuilder = new CalendarBuilder();
        // Enable relaxed-unfolding to allow ical4j parses "folding" line follows iCalendar spec
        CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);

        HttpClient client = getRemoteClient(remoteCalendar);

        java.util.Calendar from = remoteCalendar.getBeforeTime();
        java.util.Calendar to = remoteCalendar.getAfterTime();
        Map<String, String> entityTags = getEntityTags(client, remoteCalendar.getRemoteUrl(), from, to);

        // get List of event from local calendar in specific time-range
        EventQuery eventQuery = new EventQuery();
        eventQuery.setCalendarId(new String[] { remoteCalendarId });
        eventQuery.setFromDate(from);
        eventQuery.setToDate(to);

        List<CalendarEvent> eXoEvents = calService.getUserEvents(username, eventQuery);
        Iterator<CalendarEvent> it = eXoEvents.iterator();
        // events map contains set of (href, eventId) pairs in the local calendar
        Map<String, String> events = new HashMap<String, String>();
        while (it.hasNext()) {
            CalendarEvent event = it.next();
            if (Utils.isExceptionOccurrence(event))
                continue;
            events.put(calService.getCalDavResourceHref(username, remoteCalendarId, event.getId()), event.getId());
        }

        // list of href of new events on the server
        List<String> created = new ArrayList<String>();

        // map of out-of-date event/task, the key is the href of event/task, the value is the id of event on local calendar
        Map<String, String> updated = new HashMap<String, String>();

        // list of event id need to delete
        List<String> deleted = new ArrayList<String>();

        // for each event on entity tags list, find this event in local calendar by href then use etag value to get:
        Iterator<Entry<String, String>> iter = entityTags.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, String> pairs = iter.next();
            String href = pairs.getKey();
            String etag = pairs.getValue();
            // new events
            if (!events.containsKey(href)) {
                created.add(href);
            } else {
                // check need-to-update events
                String eventId = events.get(href);
                String calendarId = calService.getEvent(username, eventId).getCalendarId();
                String localEtag = calService.getCalDavResourceEtag(username, calendarId, eventId);
                if (!localEtag.equals(etag)) {
                    updated.put(href, eventId);
                }
            }
        }

        // for each event on local calendar, find this event in responses list to get list of need-to-delete event
        iter = events.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, String> pairs = iter.next();
            String href = pairs.getKey();
            if (!entityTags.containsKey(href)) {
                deleted.add(pairs.getValue());
            }
        }

        // from three lists, do update on local calendar
        // do a multi-get report request to server to get list of new events
        MultiStatus multiStatus = doCalendarMultiGet(client, remoteCalendar.getRemoteUrl(),
                created.toArray(new String[0]));
        String href;
        if (multiStatus != null) {
            for (int i = 0; i < multiStatus.getResponses().length; i++) {
                MultiStatusResponse multiRes = multiStatus.getResponses()[i];
                href = multiRes.getHref();
                DavPropertySet propSet = multiRes.getProperties(DavServletResponse.SC_OK);
                DavProperty calendarData = propSet.get(CALDAV_XML_CALENDAR_DATA, CALDAV_NAMESPACE);
                DavProperty etag = propSet.get(DavPropertyName.GETETAG.getName(), DavConstants.NAMESPACE);
                try {
                    net.fortuna.ical4j.model.Calendar iCalEvent = calendarBuilder
                            .build(new StringReader(calendarData.getValue().toString()));
                    // add new event
                    importCaldavEvent(username, remoteCalendarId, null, iCalEvent, href, etag.getValue().toString(),
                            true);
                } catch (Exception e) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Exception occurs when import calendar component " + href
                                + ". Skip this component.");
                    }
                    continue;
                }
            }
        }

        multiStatus = doCalendarMultiGet(client, remoteCalendar.getRemoteUrl(),
                updated.keySet().toArray(new String[0]));
        if (multiStatus != null) {
            for (int i = 0; i < multiStatus.getResponses().length; i++) {
                MultiStatusResponse multiRes = multiStatus.getResponses()[i];
                href = multiRes.getHref();
                DavPropertySet propSet = multiRes.getProperties(DavServletResponse.SC_OK);
                DavProperty calendarData = propSet.get(CALDAV_XML_CALENDAR_DATA, CALDAV_NAMESPACE);
                DavProperty etag = propSet.get(DavPropertyName.GETETAG.getName(), DavConstants.NAMESPACE);
                String eventId = updated.get(href);
                try {
                    net.fortuna.ical4j.model.Calendar iCalEvent = calendarBuilder
                            .build(new StringReader(calendarData.getValue().toString()));
                    // update event
                    importCaldavEvent(username, remoteCalendarId, eventId, iCalEvent, null,
                            etag.getValue().toString(), false);
                } catch (Exception e) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Exception occurs when import calendar component " + href
                                + ". Skip this component.");
                    }
                    continue;
                }
            }
        }

        // delete no-longer exists events
        Iterator<String> iterator = deleted.iterator();
        while (iterator.hasNext()) {
            String eventId = iterator.next();
            CalendarEvent event = storage_.getUserEvent(username, remoteCalendarId, eventId);
            event.setCalType(String.valueOf(Calendar.TYPE_PRIVATE));
            if (Utils.isRepeatEvent(event)) {
                storage_.removeRecurrenceSeries(username, event);
            } else {
                calService.removeUserEvent(username, remoteCalendarId, eventId);
            }
        }
        return calService.getUserCalendar(remoteCalendar.getUsername(), remoteCalendar.getCalendarId());
    }

    public MultiStatus doCalendarMultiGet(HttpClient client, String uri, String[] hrefs) throws Exception {

        if (hrefs.length == 0)
            return null;

        ReportMethod report = null;

        try {
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            Document doc = docBuilder.newDocument();

            // root element
            Element calendarMultiGet = DomUtil.createElement(doc, CALDAV_XML_CALENDAR_MULTIGET, CALDAV_NAMESPACE);
            calendarMultiGet.setAttributeNS(Namespace.XMLNS_NAMESPACE.getURI(),
                    Namespace.XMLNS_NAMESPACE.getPrefix() + ":" + DavConstants.NAMESPACE.getPrefix(),
                    DavConstants.NAMESPACE.getURI());

            ReportInfo reportInfo = new ReportInfo(calendarMultiGet, DavConstants.DEPTH_0);
            DavPropertyNameSet propNameSet = reportInfo.getPropertyNameSet();
            propNameSet.add(DavPropertyName.GETETAG);
            DavPropertyName calendarData = DavPropertyName.create(CALDAV_XML_CALENDAR_DATA, CALDAV_NAMESPACE);
            propNameSet.add(calendarData);

            Element href;
            for (int i = 0; i < hrefs.length; i++) {
                href = DomUtil.createElement(doc, DavConstants.XML_HREF, DavConstants.NAMESPACE, hrefs[i]);
                reportInfo.setContentElement(href);
            }

            report = new ReportMethod(uri, reportInfo);
            client.executeMethod(report);
            MultiStatus multiStatus = report.getResponseBodyAsMultiStatus();
            return multiStatus;
        } finally {
            if (report != null)
                report.releaseConnection();
        }
    }

    /**
     * Send a calendar-query REPORT request to CalDav server
     * @param client
     * @param uri
     * @param from
     * @param to
     * @return
     * @throws Exception
     */
    public MultiStatus doCalendarQuery(HttpClient client, String uri, java.util.Calendar from,
            java.util.Calendar to) throws Exception {
        ReportMethod report = makeCalDavQueryReport(uri, from, to);
        if (report == null)
            return null;
        try {
            client.executeMethod(report);
            MultiStatus multiStatus = report.getResponseBodyAsMultiStatus();
            return multiStatus;
        } catch (Exception e) {
            if (logger.isDebugEnabled())
                logger.debug("Exception occurs when querying calendar events from CalDav server", e);
            return null;
        } finally {
            if (report != null) {
                report.releaseConnection();
            }
        }
    }

    public void importCaldavEvent(String username, String calendarId, String eventId,
            net.fortuna.ical4j.model.Calendar iCalendar, String href, String etag, Boolean isNew) throws Exception {
        CalendarService calService = (CalendarService) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(CalendarService.class);
        if (calService == null) {
            calService = (CalendarService) PortalContainer.getInstance()
                    .getComponentInstanceOfType(CalendarService.class);
        }

        Map<String, VFreeBusy> vFreeBusyData = new HashMap<String, VFreeBusy>();
        Map<String, VAlarm> vAlarmData = new HashMap<String, VAlarm>();

        SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        format.setTimeZone(TimeZone.getTimeZone("GMT"));

        CalendarEvent original = null;
        List<CalendarEvent> exceptions = new ArrayList<CalendarEvent>();

        // import calendar components
        ComponentList componentList = iCalendar.getComponents();
        CalendarEvent exoEvent;

        if (!isNew) {
            exoEvent = storage_.getEvent(username, eventId);
            exoEvent.setCalType(String.valueOf(Calendar.TYPE_PRIVATE));

            if (Utils.isRepeatEvent(exoEvent)) {
                List<CalendarEvent> oldExceptions = storage_.getExceptionEvents(username, exoEvent);
                if (oldExceptions != null && oldExceptions.size() > 0) {
                    for (CalendarEvent exception : oldExceptions) {
                        storage_.removeUserEvent(username, calendarId, exception.getId());
                    }
                }
            }
        }

        for (Object obj : componentList) {
            if (obj instanceof VEvent) {
                VEvent event = (VEvent) obj;
                if (!event.getAlarms().isEmpty()) {
                    for (Object o : event.getAlarms()) {
                        if (o instanceof VAlarm) {
                            VAlarm va = (VAlarm) o;
                            vAlarmData.put(
                                    event.getUid().getValue() + ":" + va.getProperty(Property.ACTION).getName(),
                                    va);
                        }
                    }
                }
            }
            if (obj instanceof VFreeBusy)
                vFreeBusyData.put(((VFreeBusy) obj).getUid().getValue(), (VFreeBusy) obj);
        }

        for (Object obj : componentList) {
            if (obj instanceof VEvent) {
                VEvent event = (VEvent) obj;
                if (isNew)
                    exoEvent = new CalendarEvent();
                else
                    exoEvent = storage_.getUserEvent(username, calendarId, eventId);
                exoEvent = generateEvent(event, exoEvent, username, calendarId);

                String sValue = Utils.EMPTY_STR;
                String eValue = Utils.EMPTY_STR;
                if (event.getStartDate() != null) {
                    sValue = event.getStartDate().getValue();
                    exoEvent.setFromDateTime(event.getStartDate().getDate());
                }
                if (event.getEndDate() != null) {
                    eValue = event.getEndDate().getValue();
                    exoEvent.setToDateTime(event.getEndDate().getDate());
                }
                exoEvent = setEventAttachment(event, exoEvent, eValue, sValue);
                if (event.getProperty(Property.RECURRENCE_ID) != null) {
                    RecurrenceId recurId = (RecurrenceId) event.getProperty(Property.RECURRENCE_ID);
                    exoEvent.setRecurrenceId(format.format(new Date(recurId.getDate().getTime())));
                    if (original != null) {
                        Node originalNode = storage_.getUserCalendarHome(username).getNode(calendarId)
                                .getNode(original.getId());
                        String uuid = originalNode.getUUID();
                        exoEvent.setId(originalNode.getName());
                        exoEvent.setOriginalReference(uuid);
                        List<String> excludeId;
                        if (original.getExcludeId() != null && original.getExcludeId().length > 0) {
                            excludeId = new ArrayList<String>(Arrays.asList(original.getExcludeId()));
                        } else {
                            excludeId = new ArrayList<String>();
                        }
                        excludeId.add(exoEvent.getRecurrenceId());
                        original.setExcludeId(excludeId.toArray(new String[0]));
                        storage_.saveUserEvent(username, calendarId, original, false);
                    } else {
                        exceptions.add(exoEvent);
                    }
                    storage_.saveOccurrenceEvent(username, calendarId, exoEvent, true);
                } else {
                    if (event.getProperty(Property.RRULE) != null
                            && event.getProperty(Property.RECURRENCE_ID) == null) {
                        exoEvent = calculateEvent(event, exoEvent);
                        original = exoEvent;

                        List<String> excludeIds = new ArrayList<String>();
                        PropertyList exdates = event.getProperties(Property.EXDATE);
                        if (exdates != null && exdates.size() > 0) {
                            for (Object exdate : exdates) {
                                for (Object date : ((ExDate) exdate).getDates()) {
                                    excludeIds.add(format.format(
                                            new Date(((net.fortuna.ical4j.model.DateTime) date).getTime())));
                                }
                            }
                        }

                        if (exceptions != null && exceptions.size() > 0) {
                            for (CalendarEvent exception : exceptions) {
                                excludeIds.add(exception.getRecurrenceId());
                            }
                        }
                        exoEvent.setExcludeId(excludeIds.toArray(new String[0]));
                        storage_.saveUserEvent(username, calendarId, exoEvent, isNew);

                        String uuid = storage_.getUserCalendarHome(username).getNode(calendarId)
                                .getNode(exoEvent.getId()).getUUID();
                        if (exceptions != null && exceptions.size() > 0) {
                            for (CalendarEvent exception : exceptions) {
                                exception.setOriginalReference(uuid);
                                storage_.saveOccurrenceEvent(username, calendarId, exception, false);
                            }
                        }
                    } else {
                        storage_.saveUserEvent(username, calendarId, exoEvent, isNew);
                    }
                }
                storage_.setRemoteEvent(username, calendarId, exoEvent.getId(), href, etag);
            }

            else if (obj instanceof VToDo) {
                VToDo event = (VToDo) obj;
                exoEvent = new CalendarEvent();
                if (event.getProperty(Utils.X_STATUS) != null) {
                    exoEvent.setEventState(event.getProperty(Utils.X_STATUS).getValue());
                }
                exoEvent = setTaskAttachment(event, exoEvent, username, calendarId, vFreeBusyData);
                storage_.saveUserEvent(username, calendarId, exoEvent, isNew);
                storage_.setRemoteEvent(username, calendarId, exoEvent.getId(), href, etag);
            }
        }
    }

    public static CalendarEvent generateEvent(VEvent event, CalendarEvent exoEvent, String username,
            String calendarId) throws Exception {
        CalendarService calService = (CalendarService) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(CalendarService.class);
        if (event.getProperty(Property.CATEGORIES) != null) {
            EventCategory evCate = new EventCategory();
            evCate.setName(event.getProperty(Property.CATEGORIES).getValue().trim());
            try {
                calService.saveEventCategory(username, evCate, true);
            } catch (ItemExistsException e) {
                evCate = calService.getEventCategoryByName(username, evCate.getName());
            } catch (Exception e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Exception occurs when saving new event category '" + evCate.getName()
                            + "' for iCalendar component: " + event.getUid(), e);
                }
            }
            exoEvent.setEventCategoryId(evCate.getId());
            exoEvent.setEventCategoryName(evCate.getName());
        }
        exoEvent.setCalType(String.valueOf(Calendar.TYPE_PRIVATE));
        exoEvent.setCalendarId(calendarId);
        if (event.getSummary() != null)
            exoEvent.setSummary(event.getSummary().getValue());
        if (event.getDescription() != null)
            exoEvent.setDescription(event.getDescription().getValue());
        if (event.getStatus() != null)
            exoEvent.setStatus(event.getStatus().getValue());
        exoEvent.setEventType(CalendarEvent.TYPE_EVENT);
        return exoEvent;
    }

    public static CalendarEvent setEventAttachment(VEvent event, CalendarEvent exoEvent, String eValue,
            String sValue) throws Exception {
        if (sValue.length() == 8 && eValue.length() == 8) {
            exoEvent.setToDateTime(new Date(event.getEndDate().getDate().getTime() - 1));
        }
        if (sValue.length() > 8 && eValue.length() > 8) {
            if ("0000".equals(sValue.substring(9, 13)) && "0000".equals(eValue.substring(9, 13))) {
                exoEvent.setToDateTime(new Date(event.getEndDate().getDate().getTime() - 1));
            }
        }
        if (event.getLocation() != null)
            exoEvent.setLocation(event.getLocation().getValue());
        if (event.getPriority() != null)
            exoEvent.setPriority(CalendarEvent.PRIORITY[Integer.parseInt(event.getPriority().getValue())]);

        if (event.getProperty(Utils.X_STATUS) != null) {
            exoEvent.setEventState(event.getProperty(Utils.X_STATUS).getValue());
        }
        if (event.getClassification() != null)
            exoEvent.setPrivate(Clazz.PRIVATE.getValue().equals(event.getClassification().getValue()));
        PropertyList attendees = event.getProperties(Property.ATTENDEE);
        if (!attendees.isEmpty()) {
            String[] invitation = new String[attendees.size()];
            for (int i = 0; i < attendees.size(); i++) {
                invitation[i] = ((Attendee) attendees.get(i)).getValue();
            }
            exoEvent.setInvitation(invitation);
        }
        try {
            PropertyList dataList = event.getProperties(Property.ATTACH);
            List<Attachment> attachments = calculateAtt(dataList);
            if (!attachments.isEmpty())
                exoEvent.setAttachment(attachments);
        } catch (Exception e) {
            if (logger.isDebugEnabled()) {
                logger.debug(
                        "Exception occurs when importing attachments for iCalendar component: " + event.getUid(),
                        e);
            }
        }
        return exoEvent;
    }

    public static CalendarEvent setTaskAttachment(VToDo task, CalendarEvent exoEvent, String username,
            String calendarId, Map<String, VFreeBusy> vFreeBusyData) throws Exception {
        CalendarService calService = (CalendarService) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(CalendarService.class);
        exoEvent = new CalendarEvent();
        if (task.getProperty(Property.CATEGORIES) != null) {
            EventCategory evCate = new EventCategory();
            evCate.setName(task.getProperty(Property.CATEGORIES).getValue().trim());
            try {
                calService.saveEventCategory(username, evCate, true);
            } catch (ItemExistsException e) {
                evCate = calService.getEventCategoryByName(username, evCate.getName());
            } catch (Exception e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Exception occurs when saving new event category '" + evCate.getName()
                            + "' for CalDav event: " + task.getUid(), e);
                }
            }
            exoEvent.setEventCategoryId(evCate.getId());
            exoEvent.setEventCategoryName(evCate.getName());
        }
        exoEvent.setCalType(String.valueOf(Calendar.TYPE_PRIVATE));
        exoEvent.setCalendarId(calendarId);
        if (task.getSummary() != null)
            exoEvent.setSummary(task.getSummary().getValue());
        if (task.getDescription() != null)
            exoEvent.setDescription(task.getDescription().getValue());
        if (task.getStatus() != null)
            exoEvent.setStatus(task.getStatus().getValue());
        exoEvent.setEventType(CalendarEvent.TYPE_TASK);
        if (task.getStartDate() != null)
            exoEvent.setFromDateTime(task.getStartDate().getDate());
        if (task.getDue() != null)
            exoEvent.setToDateTime(task.getDue().getDate());
        if (task.getLocation() != null)
            exoEvent.setLocation(task.getLocation().getValue());
        if (task.getPriority() != null)
            exoEvent.setPriority(CalendarEvent.PRIORITY[Integer.parseInt(task.getPriority().getValue())]);
        if (vFreeBusyData.get(task.getUid().getValue()) != null) {
            exoEvent.setStatus(CalendarEvent.ST_BUSY);
        }
        if (task.getClassification() != null)
            exoEvent.setPrivate(Clazz.PRIVATE.getValue().equals(task.getClassification().getValue()));
        PropertyList attendees = task.getProperties(Property.ATTENDEE);
        if (!attendees.isEmpty()) {
            String[] invitation = new String[attendees.size()];
            for (int i = 0; i < attendees.size(); i++) {
                invitation[i] = ((Attendee) attendees.get(i)).getValue();
            }
            exoEvent.setInvitation(invitation);
        }
        try {
            PropertyList dataList = task.getProperties(Property.ATTACH);
            List<Attachment> attachments = calculateAtt(dataList);
            if (!attachments.isEmpty())
                exoEvent.setAttachment(attachments);
        } catch (Exception e) {
            if (logger.isDebugEnabled()) {
                logger.debug(
                        "Exception occurs when importing attachments for iCalendar component: " + task.getUid(), e);
            }
        }
        return exoEvent;
    }

    private static List<Attachment> calculateAtt(PropertyList dataList) throws Exception {
        List<Attachment> attachments = new ArrayList<Attachment>();
        for (Object o : dataList) {
            Attach a = (Attach) o;
            Attachment att = new Attachment();
            att.setName(a.getParameter(Parameter.CN).getValue());
            att.setMimeType(a.getParameter(Parameter.FMTTYPE).getValue());
            InputStream in = new ByteArrayInputStream(a.getBinary());
            att.setSize(in.available());
            att.setInputStream(in);
            attachments.add(att);
        }
        return attachments;
    }

    public static CalendarEvent calculateEvent(VEvent event, CalendarEvent exoEvent) throws Exception {
        RRule rrule = (RRule) event.getProperty(Property.RRULE);
        Recur recur = rrule.getRecur();
        String repeatType = recur.getFrequency();
        int interval = recur.getInterval();
        if (interval < 1)
            interval = 1;
        int count = recur.getCount();
        net.fortuna.ical4j.model.Date until = recur.getUntil();

        exoEvent.setRepeatInterval(interval);
        if (count > 0) {
            exoEvent.setRepeatCount(count);
            exoEvent.setRepeatUntilDate(null);
        } else {
            if (until != null) {
                Date repeatUntil = new Date(until.getTime());
                exoEvent.setRepeatUntilDate(repeatUntil);
                exoEvent.setRepeatCount(0);
            } else {
                exoEvent.setRepeatCount(0);
                exoEvent.setRepeatUntilDate(null);
            }
        }

        if (Recur.DAILY.equals(repeatType))
            exoEvent.setRepeatType(CalendarEvent.RP_DAILY);
        else if (Recur.YEARLY.equals(repeatType))
            exoEvent.setRepeatType(CalendarEvent.RP_YEARLY);
        else {
            if (Recur.WEEKLY.equals(repeatType)) {
                exoEvent.setRepeatType(CalendarEvent.RP_WEEKLY);
                WeekDayList weekDays = recur.getDayList();
                if (weekDays != null && weekDays.size() > 0) {
                    String[] byDays = new String[weekDays.size()];
                    for (int i = 0; i < byDays.length; i++) {
                        WeekDay weekDay = (WeekDay) weekDays.get(i);
                        String day = weekDay.getDay();
                        int offset = weekDay.getOffset();
                        if (offset != 0)
                            byDays[i] = String.valueOf(offset) + day;
                        else
                            byDays[i] = day;
                    }
                    exoEvent.setRepeatByDay(byDays);
                } else {
                    exoEvent.setRepeatByDay(null);
                }
            } else {
                if (Recur.MONTHLY.equals(repeatType)) {
                    exoEvent.setRepeatType(CalendarEvent.RP_MONTHLY);
                    WeekDayList weekDays = recur.getDayList();
                    if (weekDays != null && weekDays.size() > 0) {
                        String[] byDays = new String[weekDays.size()];
                        WeekDay weekDay;
                        for (int i = 0; i < byDays.length; i++) {
                            weekDay = (WeekDay) weekDays.get(i);
                            String day = weekDay.getDay();
                            int offset = weekDay.getOffset();
                            if (offset != 0)
                                byDays[i] = String.valueOf(offset) + day;
                            else
                                byDays[i] = day;
                        }
                        exoEvent.setRepeatByDay(byDays);
                        exoEvent.setRepeatByMonthDay(null);
                    } else {
                        NumberList monthdays = recur.getMonthDayList();
                        if (monthdays != null && monthdays.size() > 0) {
                            long[] byMonthDays = new long[monthdays.size()];
                            for (int i = 0; i < byMonthDays.length; i++) {
                                int monthday = (int) (Integer) monthdays.get(i);
                                byMonthDays[i] = monthday;
                            }
                            exoEvent.setRepeatByDay(null);
                            exoEvent.setRepeatByMonthDay(byMonthDays);
                        }
                    }
                }
            }
        }

        return exoEvent;
    }

    /**
     * Get the HttpClient object to prepare for the connection with remote server
     * @param remoteCalendar holds information about remote server
     * @return HttpClient object
     * @throws Exception
     */
    public HttpClient getRemoteClient(RemoteCalendar remoteCalendar) throws Exception {
        HostConfiguration hostConfig = new HostConfiguration();
        String host = new URL(remoteCalendar.getRemoteUrl()).getHost();
        if (Utils.isEmpty(host))
            host = remoteCalendar.getRemoteUrl();
        hostConfig.setHost(host);
        HttpClient client = new HttpClient();
        client.setHostConfiguration(hostConfig);
        client.getHttpConnectionManager().getParams().setConnectionTimeout(10000);
        client.getHttpConnectionManager().getParams().setSoTimeout(10000);
        // basic authentication
        if (!Utils.isEmpty(remoteCalendar.getRemoteUser())) {
            Credentials credentials = new UsernamePasswordCredentials(remoteCalendar.getRemoteUser(),
                    remoteCalendar.getRemotePassword());
            client.getState().setCredentials(new AuthScope(host, AuthScope.ANY_PORT, AuthScope.ANY_REALM),
                    credentials);
        }
        return client;
    }

    /**
     * Make the new REPORT method object to query calendar component on CalDav server
     * @param uri the URI to the calendar collection on server 
     * @param from start date of the time range to filter calendar components
     * @param to end date of the time range to filter calendar components
     * @return ReportMethod object
     * @throws Exception
     */
    public ReportMethod makeCalDavQueryReport(String uri, java.util.Calendar from, java.util.Calendar to)
            throws Exception {
        ReportMethod report = null;
        try {
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            Document doc = docBuilder.newDocument();

            // root element
            Element calendarQuery = DomUtil.createElement(doc, CALDAV_XML_CALENDAR_QUERY, CALDAV_NAMESPACE);
            calendarQuery.setAttributeNS(Namespace.XMLNS_NAMESPACE.getURI(),
                    Namespace.XMLNS_NAMESPACE.getPrefix() + ":" + DavConstants.NAMESPACE.getPrefix(),
                    DavConstants.NAMESPACE.getURI());

            ReportInfo reportInfo = new ReportInfo(calendarQuery, DavConstants.DEPTH_0);
            DavPropertyNameSet propNameSet = reportInfo.getPropertyNameSet();
            propNameSet.add(DavPropertyName.GETETAG);

            // filter element
            Element filter = DomUtil.createElement(doc, CALDAV_XML_FILTER, CALDAV_NAMESPACE);

            Element calendarComp = DomUtil.createElement(doc, CALDAV_XML_COMP_FILTER, CALDAV_NAMESPACE);
            calendarComp.setAttribute(CALDAV_XML_COMP_FILTER_NAME, net.fortuna.ical4j.model.Calendar.VCALENDAR);

            Element eventComp = DomUtil.createElement(doc, CALDAV_XML_COMP_FILTER, CALDAV_NAMESPACE);
            eventComp.setAttribute(CALDAV_XML_COMP_FILTER_NAME, net.fortuna.ical4j.model.component.VEvent.VEVENT);

            Element todoComp = DomUtil.createElement(doc, CALDAV_XML_COMP_FILTER, CALDAV_NAMESPACE);
            todoComp.setAttribute(CALDAV_XML_COMP_FILTER_NAME, net.fortuna.ical4j.model.component.VEvent.VTODO);

            Element timeRange = DomUtil.createElement(doc, CALDAV_XML_TIME_RANGE, CALDAV_NAMESPACE);
            SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
            timeRange.setAttribute(CALDAV_XML_START, format.format(from.getTime()));
            timeRange.setAttribute(CALDAV_XML_END, format.format(to.getTime()));

            eventComp.appendChild(timeRange);
            todoComp.appendChild(timeRange);
            calendarComp.appendChild(eventComp);
            calendarComp.appendChild(todoComp);
            filter.appendChild(calendarComp);

            reportInfo.setContentElement(filter);
            report = new ReportMethod(uri, reportInfo);
            return report;
        } catch (Exception e) {
            if (logger.isDebugEnabled())
                logger.debug("Cannot build report method for CalDav query", e);
            return null;
        }
    }
}