/**
 * Copyright (C) 2003-2007 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.FileNotFoundException;
import java.io.OutputStream;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.PeriodList;
import net.fortuna.ical4j.model.Recur;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.property.RRule;

import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang.StringUtils;

import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.io.XmlReader;

import org.exoplatform.calendar.service.Attachment;
import org.exoplatform.calendar.service.Calendar;
import org.exoplatform.calendar.service.CalendarEvent;
import org.exoplatform.calendar.service.CalendarImportExport;
import org.exoplatform.calendar.service.CalendarService;
import org.exoplatform.calendar.service.CalendarSetting;
import org.exoplatform.calendar.service.DataStorage;
import org.exoplatform.calendar.service.EventCategory;
import org.exoplatform.calendar.service.EventPageList;
import org.exoplatform.calendar.service.EventPageListQuery;
import org.exoplatform.calendar.service.EventQuery;
import org.exoplatform.calendar.service.FeedData;
import org.exoplatform.calendar.service.GroupCalendarData;
import org.exoplatform.calendar.service.Reminder;
import org.exoplatform.calendar.service.RemoteCalendar;
import org.exoplatform.calendar.service.RssData;
import org.exoplatform.calendar.service.Utils;
import org.exoplatform.commons.cache.future.FutureExoCache;
import org.exoplatform.commons.cache.future.Loader;
import org.exoplatform.commons.utils.ActivityTypeUtils;
import org.exoplatform.commons.utils.DateUtils;
import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.commons.utils.XPathUtils;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.CachedObjectSelector;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cache.ObjectCacheInfo;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.access.AccessControlEntry;
import org.exoplatform.services.jcr.access.PermissionType;
import org.exoplatform.services.jcr.core.ExtendedNode;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.app.SessionProviderService;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.Group;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.security.IdentityConstants;

/**
 * Created by The eXo Platform SARL Author : Hung Nguyen Quang
 * hung.nguyen@exoplatform.com Jul 10, 2007
 */
public class JCRDataStorage implements DataStorage {

  final private static String    CALENDARS           = "calendars".intern();

  final private static String    SHARED_CALENDAR     = "sharedCalendars".intern();

  final private static String    FEED                = "eXoCalendarFeed".intern();

  final private static String    CALENDAR_EVENT      = "events".intern();

  final private static String    CALENDAR_SETTING    = "calendarSetting".intern();

  final private static String    EVENT_CATEGORIES    = "eventCategories".intern();

  private final static String    VALUE               = "value".intern();

  private final NodeHierarchyCreator   nodeHierarchyCreator_;

  private final RepositoryService      repoService_;

  private final SessionProviderService sessionProviderService_;

  private  ExoCache<String, List<Calendar>> eXoGroupCalendarCache;

  private  ExoCache<String, CalendarEvent> eXoEventCache;

  /** cache for calendar events in a particular group calendar - store query as key */
  private final ExoCache<String, List<CalendarEvent>> groupCalendarEventCache;

  /** cache for calendar events in a particular group calendar - store query as key */
  private final ExoCache<String, List<CalendarEvent>> groupCalendarRecurrentEventCache;

  /** cache for event categories of users - store username as key */
  private final ExoCache<String, List<EventCategory>> userEventCategories;

  private final ExoCache<KeyValuePair, Calendar> exoCalendarCache;

  private FutureExoCache<String, CalendarEvent, JCRDataStorage> eventCache;

  /** 
   * Cache for storing calendar of type group (Public), user or shared.
   * Key = CalID (mandatory) + Username (can be null), Value = Calendar DTO
   * the key is composed of 
   * - CalID for public/group & shared calendars
   * - username for private calendars
   **/
  private FutureExoCache<KeyValuePair, Calendar, JCRDataStorage> calendarCache;

  private FutureExoCache<String, List<Calendar>, JCRDataStorage> userCalendarCache;
  private FutureExoCache<String, List<Calendar>, JCRDataStorage> groupCalendarCache;
  private FutureExoCache<String, CalendarSetting, JCRDataStorage> calendarSettingCache;

  /**
   * The map that contains all the locks
   */
  private final ConcurrentMap<String, Lock> locks = new ConcurrentHashMap<String, Lock>(64, 0.75f, 64);

  private static final Log       log                 = ExoLogger.getLogger(JCRDataStorage.class);
  private String groupHomePath;

  public JCRDataStorage(NodeHierarchyCreator nodeHierarchyCreator, RepositoryService repoService, CacheService cservice) throws Exception {
    nodeHierarchyCreator_ = nodeHierarchyCreator;
    repoService_ = repoService;
    ExoContainer container = ExoContainerContext.getCurrentContainer();
    sessionProviderService_ = (SessionProviderService) container.getComponentInstanceOfType(SessionProviderService.class);    
    
    eXoEventCache       = cservice.getCacheInstance("calendar.CalendarEvent");
    eventCache = new FutureExoCache<String, CalendarEvent, JCRDataStorage>(new CalendarEventLoader(), eXoEventCache);
    
    ExoCache<String, List<Calendar>> eXoUserCalendarCache       = cservice.getCacheInstance("calendar.UserCalendar");
    userCalendarCache = new FutureExoCache<String, List<Calendar>, JCRDataStorage>(new UserCalendarLoader(), eXoUserCalendarCache);
    
    eXoGroupCalendarCache     = cservice.getCacheInstance("calendar.GroupCalendar");
    groupCalendarCache = new FutureExoCache<String, List<Calendar>, JCRDataStorage>(new GroupCalendarLoader(), eXoGroupCalendarCache);

    ExoCache<String, CalendarSetting> eXoSettingCache = cservice.getCacheInstance("calendar.UserCalendarSetting");
    calendarSettingCache = new FutureExoCache<String, CalendarSetting, JCRDataStorage>(new CalendarSettingLoader(), eXoSettingCache);

    exoCalendarCache = cservice.getCacheInstance("calendar.Calendar");
    calendarCache = new FutureExoCache<KeyValuePair, Calendar, JCRDataStorage>(new CalendarLoader(), exoCalendarCache);

    groupCalendarEventCache = cservice.getCacheInstance("calendar.GroupCalendarEvent");    
    groupCalendarRecurrentEventCache = cservice.getCacheInstance("calendar.GroupCalendarRecurrentEvent");
    userEventCategories     = cservice.getCacheInstance("calendar.EventCategories");
  }

  /**
   * Gives the lock related to the given type and given id
   * @param type the type of the object for which we want a lock
   * @param id the unique id of the object for which we want a lock
   * @return the existing lock if a lock exist for the given id and given type
   * otherwise a new lock
   */
  private Lock getLock(String type, String id) {
    String fullId = new StringBuilder(type.length() + id.length() + 1).append(type).
        append('-').append(id).toString();
    Lock lock = locks.get(fullId);
    if (lock != null) {
      return lock;
    }
    lock = new InternalLock(fullId);
    Lock prevLock = locks.putIfAbsent(fullId, lock);
    if (prevLock != null)
    {
      lock = prevLock;
    }
    return lock;
  }

  /**
   * {@inheritDoc}
   */
  public Node getPublicCalendarServiceHome() throws Exception {
    SessionProvider sProvider = createSystemProvider();
    Node publicApp = nodeHierarchyCreator_.getPublicApplicationNode(sProvider); 
    try {
      return publicApp.getNode(Utils.CALENDAR_APP);
    } catch (Exception e) {
      Node calendarApp = publicApp.addNode(Utils.CALENDAR_APP, Utils.NT_UNSTRUCTURED);
      publicApp.getSession().save();
      return calendarApp;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getSharedCalendarHome() throws Exception {
    // have to use system session
    Node calendarServiceHome = getPublicCalendarServiceHome();
    try {
      return calendarServiceHome.getNode(SHARED_CALENDAR);
    } catch (Exception e) {
      Node sharedCal = calendarServiceHome.addNode(SHARED_CALENDAR, Utils.NT_UNSTRUCTURED);
      calendarServiceHome.getSession().save();
      return sharedCal;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getPublicRoot() throws Exception {
    SessionProvider sProvider = createSystemProvider();
    return nodeHierarchyCreator_.getPublicApplicationNode(sProvider); 
  }

  /**
   * {@inheritDoc}
   */
  public Node getUserCalendarServiceHome(String username) throws Exception {
    // SessionProvider sProvider = createSessionProvider();
    SessionProvider sProvider = createSystemProvider();
    Node userApp = nodeHierarchyCreator_.getUserApplicationNode(sProvider, username); 
    Node calendarRoot;
    try {
      return userApp.getNode(Utils.CALENDAR_APP);
    } catch (Exception e) {
      calendarRoot = userApp.addNode(Utils.CALENDAR_APP, Utils.NT_UNSTRUCTURED);
      userApp.getSession().save();
      return calendarRoot;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getPublicCalendarHome() throws Exception {
    Node calendarServiceHome = getPublicCalendarServiceHome();
    try {
      return calendarServiceHome.getNode(CALENDARS);
    } catch (Exception e) {
      Node cal = calendarServiceHome.addNode(CALENDARS, Utils.NT_UNSTRUCTURED);
      calendarServiceHome.getSession().save();
      return cal;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getUserCalendarHome(String username) throws Exception {
    Node calendarServiceHome = getUserCalendarServiceHome(username);
    try {
      return calendarServiceHome.getNode(CALENDARS);
    } catch (Exception e) {
      Node calendars = calendarServiceHome.addNode(CALENDARS, Utils.NT_UNSTRUCTURED);
      calendarServiceHome.getSession().save();
      return calendars;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getRssHome(String username) throws Exception {
    Node calendarServiceHome = getUserCalendarServiceHome(username);
    try {
      return calendarServiceHome.getNode(FEED);
    } catch (Exception e) {
      Node feed = calendarServiceHome.addNode(FEED, Utils.NT_UNSTRUCTURED);
      calendarServiceHome.getSession().save();
      return feed;
    }
  }


  /**
   * @param username
   * @return
   * @throws Exception
   */
  protected Node getEventCategoryHome(String username) throws Exception {
    Node calendarServiceHome = getUserCalendarServiceHome(username);
    try {
      return calendarServiceHome.getNode(EVENT_CATEGORIES);
    } catch (Exception e) {
      Node eventCat = calendarServiceHome.addNode(EVENT_CATEGORIES, Utils.NT_UNSTRUCTURED);
      calendarServiceHome.getSession().save();
      return eventCat;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Calendar getUserCalendar(String username, String calendarId) throws Exception {
    return getCalendar(calendarId, username);
  }

  private Calendar getCalendar(String calendarId, String username) {
    Calendar calendar = calendarCache.get(this, new KeyValuePair(calendarId, username));
    if(isNullCalendar(calendar)) {
      return null;
    }
    return (Calendar) SerializationUtils.clone(calendar);
  }

  /**
   * {@inheritDoc}
   */
  public List<Calendar> getUserCalendars(String username, boolean isShowAll) throws Exception {
    List<Calendar> calList = userCalendarCache.get(this, username);
    List<Calendar> retList = new LinkedList<Calendar>();

    if (!isShowAll) {
      List<String> defaultCalendars = Arrays.asList(getCalendarSetting(username).getFilterPrivateCalendars());

      for (Calendar calendar : calList) {
        if (!defaultCalendars.contains(calendar.getId())) {
          retList.add(calendar);
        }
      }
    } else {
      retList.addAll(calList);
    }

    return retList;
  }

  /**
   * {@inheritDoc}
   */
  public void saveUserCalendar(String username, Calendar calendar, boolean isNew) throws Exception {
    Node calendarHome = getUserCalendarHome(username);
    Node calendarNode;
    if (isNew) {
      try {
        calendarNode = calendarHome.getNode(calendar.getId());
      } catch (Exception e) {
        calendarNode = calendarHome.addNode(calendar.getId(), Utils.EXO_CALENDAR);
        calendarNode.setProperty(Utils.EXO_ID, calendar.getId());
      }
    } else {
      calendarNode = calendarHome.getNode(calendar.getId());
    }
    calendar.setCalendarOwner(username);
    calendar.setGroups(null);
    setCalendarProperties(calendarNode, calendar);
    Session session = calendarHome.getSession();
    session.save();

    userCalendarCache.remove(username);
    clearCalendarCache(calendar.getId(), username);
  }

  /**
   * {@inheritDoc}
   */
  public Calendar removeUserCalendar(String username, String calendarId) throws Exception {
    Node calendarHome = getUserCalendarHome(username);
    if (calendarHome.hasNode(calendarId)) {
      Node calNode = calendarHome.getNode(calendarId);
      Calendar calendar = getCalendar(new String[] { calendarId }, username, calNode, true);
      NodeIterator iter = calNode.getNodes();
      try {
        while (iter.hasNext()) {
          Node eventNode = iter.nextNode();
          Node eventFolder = getEventFolder(eventNode.getProperty(Utils.EXO_FROM_DATE_TIME)
                                            .getDate()
                                            .getTime());
          syncRemoveEvent(eventFolder, eventNode.getName());
          removeReminder(eventNode);
        }
        calNode.remove();
        calendarHome.save();
      } catch (Exception e) {
        log.error("Exception occurred when removing calendar " + calendarId, e);
      }
      eXoEventCache.select(new EventCalendarSelector(calendarId));
      userCalendarCache.remove(username);
      clearCalendarCache(calendarId, username);

      try {
        removeFeed(username, calendarId);
      } catch (Exception e) {
        log.warn("Exception occurred when removing feeds from calendar " + calendarId, e);
      }
      return calendar;
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public Calendar getGroupCalendar(String calendarId) throws Exception {
    return getCalendar(calendarId, null);
  }

  private String buildGroupCalendarQuery(String groupId) {
    StringBuilder queryString = new StringBuilder("/jcr:root").append(getGroupCalendarHomePath()).append("/element(*,exo:calendar)[@exo:groups='").append(groupId).append("']");
    return queryString.toString();
  }

  /**
   * {@inheritDoc}
   */
  public List<GroupCalendarData> getGroupCalendars(String[] groupIds,
                                                   boolean isShowAll,
                                                   String username) throws Exception {
    List<GroupCalendarData> groupCalendars = new LinkedList<GroupCalendarData>();

    String sQuery;

    for (String groupId : groupIds) {
      sQuery = buildGroupCalendarQuery(groupId);

      List<Calendar> calList = groupCalendarCache.get(this, sQuery);      
      if (calList != null && !calList.isEmpty()) {
        String[] defaultCalendars = null;
        if (username != null) {
          CalendarSetting calendarSetting = getCalendarSetting(username);
          if (calendarSetting != null)
            defaultCalendars = calendarSetting.getFilterPublicCalendars();
        }
        
        List<Calendar> calendars = new LinkedList<Calendar>();
        for (Calendar c : calList) {
          Calendar cal = getCalendar(defaultCalendars, null, c, isShowAll);
          if (cal != null)
            calendars.add(cal);
        }
        groupCalendars.add(new GroupCalendarData(groupId, groupId, calendars));
      }
    }
    return groupCalendars;
  }

  private String getGroupCalendarHomePath() {
    if (groupHomePath == null) {
      try {
        groupHomePath = getPublicCalendarHome().getPath();
      } catch (Exception e) {
        groupHomePath = null;
        log.error("Can't find group calendar home path", e);
      }
    }
    return groupHomePath;
  }
  


  /**
   * {@inheritDoc}
   */
  public void savePublicCalendar(Calendar calendar, boolean isNew, String username) throws Exception {
    Node calendarHome = getPublicCalendarHome();
    Node calendarNode;
    if (isNew) {
      if (calendarHome.hasNode(calendar.getId()))
        throw new Exception("This calendar is already exists");
      calendarNode = calendarHome.addNode(calendar.getId(), Utils.EXO_CALENDAR);
      calendarNode.setProperty(Utils.EXO_ID, calendar.getId());
    } else {
      calendarNode = calendarHome.getNode(calendar.getId());
    }
    String[] oldCalendarGroups = loadCalendar(calendarNode).getGroups();
    String[] calendarGroups = calendar.getGroups();
    setCalendarProperties(calendarNode, calendar);
    calendarHome.getSession().save();
    // Clear the cache to avoid inconsistency

    Set<String> keys = new HashSet<>();
    if(calendarGroups != null){
      for (String key : calendarGroups) {
        keys.add(buildGroupCalendarQuery(key));
      }
    }
    if(oldCalendarGroups != null){
      for (String key : oldCalendarGroups) {
        keys.add(buildGroupCalendarQuery(key));
      }
    }

    eXoGroupCalendarCache.select(new GroupCalendarSelector(keys));

    clearCalendarCache(calendar.getId(), username);
  }

  /**
   * {@inheritDoc}
   */
  public Calendar removeGroupCalendar(String calendarId) throws Exception {
    Node calendarHome = getPublicCalendarHome();
    if (calendarHome.hasNode(calendarId)) {
      Node calNode = calendarHome.getNode(calendarId);
      Calendar calendar = getCalendar(new String[] { calendarId }, null, calNode, true);
      NodeIterator iter = calNode.getNodes();
      while (iter.hasNext()) {
        Node eventNode = iter.nextNode();
        Node eventFolder = getEventFolder(eventNode.getProperty(Utils.EXO_FROM_DATE_TIME)
                                          .getDate()
                                          .getTime());
        removeReminder(eventNode);
        syncRemoveEvent(eventFolder, eventNode.getName());
      }
      calNode.remove();
      calendarHome.getSession().save();
      eXoEventCache.select(new EventCalendarSelector(calendarId));
      // Clear the cache to avoid inconsistency
      String[] groupIds = calendar.getGroups();
      if(groupIds != null){
        for (String groupId : groupIds) {
          String queryKey = buildGroupCalendarQuery(groupId);
          groupCalendarCache.remove(queryKey);
        }
      }

      clearCalendarCache(calendar.getId(), calendar.getCalendarOwner());
      return calendar;
    }
    return null;
  }

  public Calendar getCalendar(String[] defaultFilterCalendars, String username, Node calNode, boolean isShowAll) throws Exception {
    if (!isShowAll && defaultFilterCalendars != null && Arrays.asList(defaultFilterCalendars).contains(calNode.getName()))
      return null;
    return loadCalendar(calNode);
  }

  private Calendar getCalendar(String[] defaultFilterCalendars, String username, Calendar cal, boolean isShowAll) throws Exception {
    if (!isShowAll && defaultFilterCalendars != null && Arrays.asList(defaultFilterCalendars).contains(cal.getId()))
      return null;
    return cal;
  }

  public Calendar loadCalendar(Node calNode) throws Exception {
    Calendar calendar = new Calendar();
    StringBuilder namePattern = new StringBuilder(256);
    namePattern.append(Utils.EXO_ID).append('|').append(Utils.EXO_NAME).append('|').append(Utils.EXO_DESCRIPTION)
    .append('|').append(Utils.EXO_LOCALE).append('|')
    .append(Utils.EXO_TIMEZONE).append('|').append(Utils.EXO_SHARED_COLOR).append('|')
    .append(Utils.EXO_CALENDAR_COLOR).append('|').append(Utils.EXO_CALENDAR_OWNER).append('|')
    .append(Utils.EXO_PUBLIC_URL).append('|').append(Utils.EXO_PRIVATE_URL).append('|').append(Utils.EXO_GROUPS)
    .append('|').append(Utils.EXO_VIEW_PERMISSIONS).append('|').append(Utils.EXO_EDIT_PERMISSIONS).append("|").append(Utils.EXO_DATE_MODIFIED);
    PropertyIterator it = calNode.getProperties(namePattern.toString());
    List<String> groups = null;
    String[] viewPermission = null, editPermission = null;
    while (it.hasNext()) {
      Property p = it.nextProperty();
      String name = p.getName();
      if (name.equals(Utils.EXO_ID)) {
        calendar.setId(p.getString());
      } else if (name.equals(Utils.EXO_NAME)) {
        calendar.setName(p.getString());
      } else if (name.equals(Utils.EXO_DESCRIPTION)) {
        calendar.setDescription(p.getString());
      } else if (name.equals(Utils.EXO_LOCALE)) {
        calendar.setLocale(p.getString());
      } else if (name.equals(Utils.EXO_TIMEZONE)) {
        calendar.setTimeZone(p.getString());
      } else if (name.equals(Utils.EXO_SHARED_COLOR)) {
        calendar.setCalendarColor(p.getString());
      } else if (name.equals(Utils.EXO_CALENDAR_COLOR)) {
        calendar.setCalendarColor(p.getString());
      } else if (name.equals(Utils.EXO_CALENDAR_OWNER)) {
        calendar.setCalendarOwner(p.getString());
      } else if (name.equals(Utils.EXO_PUBLIC_URL)) {
        calendar.setPublicUrl(p.getString());
      } else if (name.equals(Utils.EXO_PRIVATE_URL)) {
        calendar.setPrivateUrl(p.getString());
      } else if (name.equals(Utils.EXO_DATE_MODIFIED)) {
        calendar.setLastModified(p.getDate().getTimeInMillis());
      } else if (name.equals(Utils.EXO_GROUPS)) {
        Value[] values = p.getValues();
        groups = new ArrayList<String>();
        for (Value v : values) {
          groups.add(v.getString());
        }
      } else if (name.equals(Utils.EXO_VIEW_PERMISSIONS)) {
        viewPermission = ValuesToStrings(p.getValues());
      } else if (name.equals(Utils.EXO_EDIT_PERMISSIONS)) {
        editPermission = ValuesToStrings(p.getValues());
      }
    }
    calendar.setRemote(calNode.isNodeType(Utils.EXO_REMOTE_MIXIN));
    calendar.setHasChildren(calNode.hasNodes());
    if (!calendar.isPublic()) {
      if (groups != null) {
        calendar.setGroups(groups.toArray(new String[groups.size()]));
      }
      if (viewPermission != null) {
        calendar.setViewPermission(viewPermission);
      }
      if (editPermission != null) {
        calendar.setEditPermission(editPermission);
      }
    }
    if (calNode.getPath().startsWith(getGroupCalendarHomePath())) {
      calendar.setCalType(Calendar.Type.GROUP.type());
    } else {
      calendar.setCalType(Calendar.Type.PERSONAL.type());
    }
    return calendar;
  }

  /**=== Event Category APIs ===*/

  public List<EventCategory> getEventCategories(String username) throws Exception {
    List<EventCategory> eventCategoryList;
    Lock lock = getLock("EventCategories", username);
    lock.lock();

    try {
      eventCategoryList = userEventCategories.get(username);
      if (eventCategoryList == null) {
        /** query new */
        eventCategoryList = new ArrayList<EventCategory>();
        Node eventCategoryHome = getEventCategoryHome(username);
        NodeIterator iter = eventCategoryHome.getNodes();

        while (iter.hasNext()) {
          eventCategoryList.add(getEventCategory(iter.nextNode()));
        }
        userEventCategories.put(username, eventCategoryList);
      }
    } finally {
      lock.unlock();
    }

    return eventCategoryList;
  }

  public void saveEventCategory(String username,
                                EventCategory eventCategory,
                                String[] values,
                                boolean isNew) throws Exception {
    saveEventCategory(username, eventCategory, isNew);
  }

  public void saveEventCategory(String username, EventCategory eventCategory, boolean isNew) throws Exception {
    Node eventCategoryHome = getEventCategoryHome(username);
    Node eventCategoryNode = null;
    if (isNew) {
      eventCategoryNode = eventCategoryHome.addNode(eventCategory.getId(), Utils.EXO_EVENT_CATEGORY);
    } else {

      eventCategoryNode = eventCategoryHome.getNode(eventCategory.getId());
      Node calendarHome =  getUserCalendarHome(username);
      QueryManager qm = calendarHome.getSession().getWorkspace().getQueryManager();
      NodeIterator calIter = calendarHome.getNodes();
      Query query;
      QueryResult result;
      while (calIter.hasNext()) {
        StringBuilder queryString = new StringBuilder("/jcr:root").append(XPathUtils.escapeIllegalXPathName(calIter.nextNode().getPath()))
            .append("//element(*,exo:calendarEvent)[@exo:eventCategoryId='").append(eventCategory.getId())
            .append("']");
        query = qm.createQuery(queryString.toString(), Query.XPATH);
        result = query.execute();
        NodeIterator it = result.getNodes();
        while (it.hasNext()) {
          Node eventNode = it.nextNode();
          eventNode.setProperty(Utils.EXO_EVENT_CATEGORY_NAME, eventCategory.getName());
        }
      }

      if (getSharedCalendarHome().hasNode(username)) {
        PropertyIterator iterPro = getSharedCalendarHome().getNode(username).getReferences();
        while (iterPro.hasNext()) {
          try {
            Node calendar = iterPro.nextProperty().getParent();
            NodeIterator it = calendar.getNodes();
            while (it.hasNext()) {
              Node eventNode = it.nextNode();
              if (eventNode.hasProperty(Utils.EXO_EVENT_CATEGORYID))
                if (eventNode.getProperty(Utils.EXO_EVENT_CATEGORYID)
                    .getString()
                    .equals(eventCategory.getId()))
                  eventNode.setProperty(Utils.EXO_EVENT_CATEGORY_NAME, eventCategory.getName());
            }

          } catch (Exception e) {
            if (log.isDebugEnabled())
              log.debug(e);
          }
        }
      }
      eXoEventCache.select(new EventCategorySelector(eventCategory.getId()));
    }
    eventCategoryNode.setProperty(Utils.EXO_ID, eventCategory.getId());
    eventCategoryNode.setProperty(Utils.EXO_NAME, eventCategory.getName());
    eventCategoryHome.getSession().save();

    Lock lock = getLock("EventCategories", username);
    lock.lock();
    try {
      userEventCategories.put(username, null);
    } finally {
      lock.unlock();
    }
  }

  public void removeEventCategory(String username, String eventCategoryId) throws Exception {
    Node eventCategoryHome = getEventCategoryHome(username);
    if (eventCategoryHome.hasNode(eventCategoryId)) {
      Node eventCategoryNode = eventCategoryHome.getNode(eventCategoryId);
      for (CalendarEvent ce : getUserEventByCategory(username, eventCategoryId)) {
        ce.setEventCategoryId(CalendarService.DEFAULT_EVENTCATEGORY_ID_ALL);
        ce.setEventCategoryName(CalendarService.DEFAULT_EVENTCATEGORY_NAME_ALL);
        saveUserEvent(username, ce.getCalendarId(), ce, false);
      }
      for (CalendarEvent ce : getSharedEventByCategory(username, eventCategoryId)) {
        ce.setEventCategoryId(CalendarService.DEFAULT_EVENTCATEGORY_ID_ALL);
        ce.setEventCategoryName(CalendarService.DEFAULT_EVENTCATEGORY_NAME_ALL);
        saveEventToSharedCalendar(username, ce.getCalendarId(), ce, false);
      }
      for (CalendarEvent ce : getPublicEventByCategory(username, eventCategoryId)) {
        ce.setEventCategoryId(CalendarService.DEFAULT_EVENTCATEGORY_ID_ALL);
        ce.setEventCategoryName(CalendarService.DEFAULT_EVENTCATEGORY_NAME_ALL);
        savePublicEvent(ce.getCalendarId(), ce, false);
      }
      eventCategoryNode.remove();
      eventCategoryHome.save();
      eventCategoryHome.getSession().save();

      Lock lock = getLock("EventCategories", username);
      lock.lock();
      try {
        userEventCategories.put(username, null);
      } finally {
        lock.unlock();
      }    }
  }

  /**
   * {@inheritDoc}
   */
  public EventCategory getEventCategory(Node eventCatNode) throws Exception {
    EventCategory eventCategory = new EventCategory();
    StringBuilder namePattern = new StringBuilder(128);
    namePattern.append(Utils.EXO_ID).append('|').append(Utils.EXO_NAME).append('|').append(Utils.EXO_DESCRIPTION).append("|")
      .append(Utils.EXO_DATE_MODIFIED);
    PropertyIterator it = eventCatNode.getProperties(namePattern.toString());
    while (it.hasNext()) {
      Property p = it.nextProperty();
      String name = p.getName();
      if (name.equals(Utils.EXO_ID)) {
        eventCategory.setId(p.getString());
      } else if (name.equals(Utils.EXO_NAME)) {
        eventCategory.setName(p.getString());
      } else if (name.equals(Utils.EXO_DATE_MODIFIED)) {
        eventCategory.setLastModified(p.getDate().getTimeInMillis());
      }
    }
    return eventCategory;
  }

  public EventCategory getEventCategory(String username, String eventcategoryId) throws Exception {
    Node eventCategoryHome = getEventCategoryHome(username);
    return getEventCategory(eventCategoryHome.getNode(eventcategoryId));
  }

  public CalendarEvent getUserEvent(String username, String calendarId, String eventId) throws Exception {
    Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
    return getEvent(calendarNode.getNode(eventId));
  }

  public List<CalendarEvent> getUserEventByCalendar(String username, List<String> calendarIds) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    for (String calendarId : calendarIds) {
      Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
      NodeIterator it = calendarNode.getNodes();
      while (it.hasNext()) {
        events.add(this.getEventById(it.nextNode().getProperty(Utils.EXO_ID).getString()));
      }
    }
    return events;
  }

  public List<CalendarEvent> getPublicEventByCategory(String username, String eventCategoryId) throws Exception {
    Node publicCalendarHome = getPublicCalendarHome();
    QueryManager qm = publicCalendarHome.getSession().getWorkspace().getQueryManager();
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    try {
      Query query;
      QueryResult result;
      NodeIterator calIter = publicCalendarHome.getNodes();
      while (calIter.hasNext()) {
        StringBuilder queryString = new StringBuilder("/jcr:root").append(calIter.nextNode().getPath())
            .append("//element(*,exo:calendarEvent)[@exo:eventCategoryId='").append(eventCategoryId)
            .append("']");
        query = qm.createQuery(queryString.toString(), Query.XPATH);
        result = query.execute();
        NodeIterator it = result.getNodes();
        while (it.hasNext()) {
          events.add(getEvent(it.nextNode()));
        }
      }
    } catch (Exception e) {
      log.error("Error occurred when querying public events from event category " + eventCategoryId,
                e);
    }
    return events;
  }

  public List<CalendarEvent> getSharedEventByCategory(String username, String eventCategoryId) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    try {
      if (getSharedCalendarHome().hasNode(username)) {
        PropertyIterator iterPro = getSharedCalendarHome().getNode(username).getReferences();
        while (iterPro.hasNext()) {
          try {
            Node calendar = iterPro.nextProperty().getParent();
            NodeIterator it = calendar.getNodes();
            while (it.hasNext()) {
              Node eventNode = it.nextNode();
              if (eventNode.hasProperty(Utils.EXO_EVENT_CATEGORYID))
                if (eventNode.getProperty(Utils.EXO_EVENT_CATEGORYID)
                    .getString()
                    .equals(eventCategoryId)) {
                  events.add(getEvent(eventNode));
                }
            }

          } catch (Exception e) {
            if (log.isDebugEnabled())
              log.debug(e);
          }
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
    }
    return events;
  }

  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getUserEventByCategory(String username, String eventCategoryId) throws Exception {
    Node calendarHome = getUserCalendarHome(username);
    QueryManager qm = calendarHome.getSession().getWorkspace().getQueryManager();
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    Query query;
    QueryResult result;
    NodeIterator calIter = calendarHome.getNodes();
    while (calIter.hasNext()) {
      StringBuilder queryString = new StringBuilder("/jcr:root").append(XPathUtils.escapeIllegalXPathName(calIter.nextNode().getPath()))
          .append("//element(*,exo:calendarEvent)[@exo:eventCategoryId='").append(eventCategoryId)
          .append("']");
      query = qm.createQuery(queryString.toString(), Query.XPATH);
      result = query.execute();
      NodeIterator it = result.getNodes();
      while (it.hasNext()) {
        events.add(getEvent(it.nextNode()));
      }
    }
    return events;
  }

  private CalendarEvent getEventById(Node calendarHome, String eventId) throws Exception {
    String queryString = new StringBuilder("/jcr:root").append(XPathUtils.escapeIllegalXPathName(calendarHome.getPath()))
        .append("//element(*,exo:calendarEvent)[@exo:id='").append(eventId).append("']").toString();
    QueryManager qm = calendarHome.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString, Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator it = result.getNodes();
    if (it.hasNext())
      return getEvent(it.nextNode());
    else
      return null;
  }

  /**
   * {@inheritDoc}
   */
  public CalendarEvent getEvent(String username, String eventId) throws Exception {
    Node calendarHome = getUserCalendarHome(username);
    return getEventById(calendarHome, eventId);
  }

  private List<CalendarEvent> getEventsByType(Node calendarHome, int type, EventQuery eventQuery) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    eventQuery.setCalendarPath(calendarHome.getPath());      
    QueryManager qm = calendarHome.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(eventQuery.getQueryStatement(), eventQuery.getQueryType());
    QueryResult result = query.execute();
    NodeIterator it = result.getNodes();
    CalendarEvent calEvent;
    while (it.hasNext()) {
      calEvent = getEvent(it.nextNode());
      calEvent.setCalType(String.valueOf(type));
      events.add(calEvent);
      if (eventQuery.getLimitedItems() == it.getPosition())
        break;
    }
    return events;
  }

  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getUserEvents(String username, EventQuery eventQuery) throws Exception {
    Node calendarHome = getUserCalendarHome(username);      
    return getEventsByType(calendarHome, Calendar.TYPE_PRIVATE, eventQuery);
  }

  /**
   * {@inheritDoc}
   */
  public void saveUserEvent(String username, String calendarId, CalendarEvent event, boolean isNew) throws Exception {
    Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
    event.setCalendarId(calendarId); // make sur the event is attached to the
    if (event.getReminders() != null && event.getReminders().size() > 0) {
      try {
        Node reminderFolder = getReminderFolder(event.getFromDateTime());
        saveEvent(calendarNode, event, reminderFolder, isNew);
      } catch (Exception e) {
        if (log.isDebugEnabled())
          log.debug(e);
      }
    } else {
      saveEvent(calendarNode, event, null, isNew);
    }
    userCalendarCache.remove(username);
  }

  /**
   * Save a occurrence event
   * 
   * @param username
   * @param calendarId
   * @param event
   * @param isNew
   * @throws Exception
   */
  public void saveOccurrenceEvent(String username,
                                  String calendarId,
                                  CalendarEvent event,
                                  boolean isNew) throws Exception {
    int calType = getTypeOfCalendar(username, calendarId);
    if (calType == Calendar.TYPE_PRIVATE) {
      Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
      event.setCalendarId(calendarId);
      if (event.getReminders() != null && event.getReminders().size() > 0) {
        Node reminderFolder = getReminderFolder(event.getFromDateTime());
        saveOccurrenceEvent(calendarNode, event, reminderFolder, isNew);
      } else {
        saveOccurrenceEvent(calendarNode, event, null, isNew);
      }
      return;
    }

    if (calType == Calendar.TYPE_SHARED) {
      Node sharedCalendarHome = getSharedCalendarHome();
      if (sharedCalendarHome.hasNode(username)) {
        Node userNode = sharedCalendarHome.getNode(username);
        PropertyIterator iter = userNode.getReferences();
        Node calendar;
        while (iter.hasNext()) {
          calendar = iter.nextProperty().getParent();
          if (calendar.getProperty(Utils.EXO_ID).getString().equals(calendarId)) {
            if (!canEdit(calendar, username)) {
              if (log.isDebugEnabled())
                log.debug("\n Do not have edit permission. \n");
              throw new AccessDeniedException();
            }
            Node reminderFolder = getReminderFolder(event.getFromDateTime());
            saveOccurrenceEvent(calendar, event, reminderFolder, isNew);
            break;
          }
        }
      }
      return;
    }

    if (calType == Calendar.TYPE_PUBLIC) {
      Node calendarNode = getPublicCalendarHome().getNode(calendarId);
      Node reminderFolder = getReminderFolder(event.getFromDateTime());
      saveOccurrenceEvent(calendarNode, event, reminderFolder, isNew);
      return;
    }
  }

  public void saveOccurrenceEvent(Node calendarNode,
                                  CalendarEvent event,
                                  Node reminderFolder,
                                  boolean isNew) throws Exception {
    try {
      if (isNew) {
        // convert a 'virtual' occurrence to 'exception' occurrence
        Node originalNode = null;
        try {
          originalNode = calendarNode.getNode(event.getId());
        } catch (PathNotFoundException e) {
          if (log.isDebugEnabled())
            log.debug("Original recurrence node not found", e);
        }
        event.setId("Event" + IdGenerator.generate());
        event.setRepeatType(CalendarEvent.RP_NOREPEAT);

        // set reference to original node
        if (originalNode != null) {
          event.setOriginalReference(originalNode.getUUID());
        }

        event.setIsExceptionOccurrence(true);
        event.setRepeatInterval(0);
        event.setRepeatCount(0);
        event.setRepeatUntilDate(null);
        event.setRepeatByDay(null);
        event.setRepeatByMonthDay(null);

        saveEvent(calendarNode, event, reminderFolder, true);
      } else {
        event.setRepeatType(CalendarEvent.RP_NOREPEAT);
        event.setIsExceptionOccurrence(true);
        event.setRepeatByDay(null);
        event.setRepeatCount(0);
        event.setRepeatInterval(0);
        event.setRepeatUntilDate(null);
        event.setRepeatByMonthDay(null);
        saveEvent(calendarNode, event, reminderFolder, false);
      }
    } catch (Exception e) {
      log.error("Error occurred when saving occurrence event", e);
    }
  }

  public void removeOccurrenceInstance(String username, CalendarEvent occurrence) throws Exception {
    String eventId = occurrence.getId();
    String calendarId = occurrence.getCalendarId();
    int calType = Integer.parseInt(occurrence.getCalType());
    String recurId = occurrence.getRecurrenceId();
    CalendarEvent originalEvent = null;

    // get the original event
    if (calType == Calendar.TYPE_PRIVATE)
      originalEvent = getUserEvent(username, calendarId, eventId);
    else if (calType == Calendar.TYPE_PUBLIC)
      originalEvent = getGroupEvent(calendarId, eventId);
    else if (calType == Calendar.TYPE_SHARED)
      originalEvent = getSharedEvent(username, calendarId, eventId);

    // then update the exludeId property: add recurId to this list
    String[] excludeId;
    if (originalEvent.getExcludeId() == null) {
      excludeId = new String[] { recurId };
    } else {
      List<String> excId = new ArrayList<String>(Arrays.asList(originalEvent.getExcludeId()));
      excId.add(recurId);
      excludeId = excId.toArray(new String[0]);
    }
    originalEvent.setExcludeId(excludeId);

    if (calType == Calendar.TYPE_PRIVATE) {
      saveUserEvent(username, calendarId, originalEvent, false);
      return;
    }
    if (calType == Calendar.TYPE_PUBLIC) {
      savePublicEvent(calendarId, originalEvent, false);
      return;
    }
    if (calType == Calendar.TYPE_SHARED) {
      saveEventToSharedCalendar(username, calendarId, originalEvent, false);
      return;
    }
  }

  public void removeRecurrenceSeries(String username, CalendarEvent originalEvent) throws Exception {
    int calType = Integer.parseInt(originalEvent.getCalType());
    if (originalEvent.getRepeatType().equals(CalendarEvent.RP_NOREPEAT))
      return;

    List<CalendarEvent> exceptions = getExceptionEvents(username, originalEvent);
    if (exceptions != null && exceptions.size() > 0) {
      for (CalendarEvent exception : exceptions) {
        removeReference(exception);  // remove reference to original event to avoid ReferentialIntegrityException 
      }
    }

    // delete original node
    if (calType == Calendar.TYPE_PRIVATE) {
      removeUserEvent(username, originalEvent.getCalendarId(), originalEvent.getId());
      return;
    }
    if (calType == Calendar.TYPE_PUBLIC) {
      removePublicEvent(originalEvent.getCalendarId(), originalEvent.getId());
      return;
    }
    if (calType == Calendar.TYPE_SHARED) {
      removeSharedEvent(username, originalEvent.getCalendarId(), originalEvent.getId());
      return;
    }
  }
  // removes reference of an exception event to the original event 
  private void removeReference(CalendarEvent exceptionEvent) throws Exception {
    Node calendarApp = Utils.getPublicServiceHome(Utils.createSystemProvider());
    QueryManager queryManager = calendarApp.getSession().getWorkspace().getQueryManager();
    String sql = "select * from exo:repeatCalendarEvent where exo:id=" + "\'" + exceptionEvent.getId() + "\'";
    Query query = queryManager.createQuery(sql, Query.SQL);
    QueryResult result = query.execute();
    NodeIterator nodesIt = result.getNodes();
    if(nodesIt.hasNext()) {
      Node eventNode = nodesIt.nextNode();
      eventNode.setProperty(Utils.EXO_ORIGINAL_REFERENCE, (Value)null);
      eventNode.getSession().save();
      eventCache.remove(eventNode.getProperty(Utils.EXO_ID).getString());
    }
  }

  public Node getCalendarEventNode(String username,
                                   String calType,
                                   String calendarId,
                                   String eventId) throws Exception {
    Node eventNode = null;
    try {
      if (String.valueOf(Calendar.TYPE_PRIVATE).equals(calType))
        eventNode = getUserCalendarHome(username).getNode(calendarId).getNode(eventId);
      else if (String.valueOf(Calendar.TYPE_PUBLIC).equals(calType))
        eventNode = getPublicCalendarHome().getNode(calendarId).getNode(eventId);
      else if (String.valueOf(Calendar.TYPE_SHARED).equals(calType)) {
        Node sharedCalendarHome = getSharedCalendarHome();
        if (sharedCalendarHome.hasNode(username)) {
          PropertyIterator iter = sharedCalendarHome.getNode(username).getReferences();
          Node calendar;
          while (iter.hasNext()) {
            calendar = iter.nextProperty().getParent();
            if (!calendarId.equals(calendar.getProperty(Utils.EXO_ID)))
              continue;
            if (calendar.hasNode(eventId)) {
              eventNode = calendar.getNode(eventId);
              break;
            }
          }
        }
      }
      return eventNode;
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug("Exception occurs when get calendar event node", e);
      return null;
    }

  }

  /**
   * {@inheritDoc}
   */
  public CalendarEvent removeUserEvent(String username, String calendarId, String eventId) throws Exception {
    Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
    if (calendarNode.hasNode(eventId)) {
      Node eventNode = calendarNode.getNode(eventId);
      CalendarEvent event = getEvent(eventNode);
      // Need to use system session
      try {
        Node eventFolder = getEventFolder(event.getFromDateTime());
        syncRemoveEvent(eventFolder, event.getId());
      } catch (Exception e) {
        if (log.isDebugEnabled())
          log.debug(e);
      }
      removeReminder(eventNode);
      eventNode.remove();
      calendarNode.save();
      calendarNode.getSession().save();
      calendarNode.refresh(true);
      userCalendarCache.remove(username);
      return event;
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public void removeReminder(Node eventNode) throws Exception {
    if (eventNode.hasProperty(Utils.EXO_FROM_DATE_TIME)) {
      try {
        Node reminders = getReminderFolder(eventNode.getProperty(Utils.EXO_FROM_DATE_TIME)
                                           .getDate()
                                           .getTime());
        try {
          reminders.getNode(eventNode.getName()).remove();
          reminders.save();
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Failed to remove reminder for an event", e);
          }
        }
        Node events = reminders.getParent().getNode(Utils.CALENDAR_REMINDER);
        if (events != null && events.hasNode(eventNode.getName())) {
          if (events.hasNode(eventNode.getName())) {
            events.getNode(eventNode.getName()).remove();
            if (!reminders.isNew())
              reminders.save();
            else
              reminders.getSession().save();
          }
        }
        eventCache.remove(eventNode.getProperty(Utils.EXO_ID).getString());
      } catch (Exception e) {
        if (log.isDebugEnabled())
          log.debug(e);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public CalendarEvent getGroupEvent(String calendarId, String eventId) throws Exception {
    Node calendarNode = getPublicCalendarHome().getNode(calendarId);
    CalendarEvent calEvent = getEvent(calendarNode.getNode(eventId));
    calEvent.setCalType(String.valueOf(Calendar.TYPE_PUBLIC));
    return calEvent;
  }

  /**
   * {@inheritDoc}
   */
  public CalendarEvent getGroupEvent(String eventId) throws Exception {
    Node calendarHome = getPublicCalendarHome();
    return getEventById(calendarHome, eventId);
  }

  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getGroupEventByCalendar(List<String> calendarIds) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    for (String calendarId : calendarIds) {
      Node calendarNode = getPublicCalendarHome().getNode(calendarId);
      NodeIterator it = calendarNode.getNodes();
      while (it.hasNext()) {
        CalendarEvent event = this.getEventById(it.nextNode().getProperty(Utils.EXO_ID).getString());
        event.setCalType(String.valueOf(Calendar.TYPE_PUBLIC));
        events.add(event);
      }
    }
    return events;
  }

  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getPublicEvents(EventQuery eventQuery) throws Exception {
    Node calendarHome = getPublicCalendarHome();
    List<CalendarEvent> events = getEventsByType(calendarHome, Calendar.TYPE_PUBLIC, eventQuery);
    return events;
  }

  /**
   * {@inheritDoc}
   */
  public void savePublicEvent(String calendarId, CalendarEvent event, boolean isNew) throws Exception {
    Node calendarNode = getPublicCalendarHome().getNode(calendarId);
    event.setCalendarId(calendarId);
    event.setCalType(String.valueOf(Calendar.TYPE_PUBLIC));
    Node reminderFolder = getReminderFolder(event.getFromDateTime());
    saveEvent(calendarNode, event, reminderFolder, isNew);

    groupCalendarEventCache.select(new GroupCalendarEventSelector(calendarNode.getPath()));
    groupCalendarRecurrentEventCache.select(new GroupCalendarEventSelector(calendarNode.getPath()));
  }

  /**
   * {@inheritDoc}
   */
  public CalendarEvent removePublicEvent(String calendarId, String eventId) throws Exception {
    Node calendarNode = getPublicCalendarHome().getNode(calendarId);
    if (calendarNode.hasNode(eventId)) {
      Node eventNode = calendarNode.getNode(eventId);
      CalendarEvent event = getEvent(eventNode);
      removeReminder(eventNode);
      eventNode.remove();
      calendarNode.save();
      calendarNode.getSession().save();
      calendarNode.refresh(true);
      try {
        Node eventFolder = getEventFolder(event.getFromDateTime());
        syncRemoveEvent(eventFolder, eventId);
      } catch (Exception e) {
        if (log.isDebugEnabled())
          log.debug(e.getMessage());
      }

      Calendar calendar = loadCalendar(calendarNode);
      String[] calendarGroups = calendar.getGroups();
      if(calendarGroups != null){
        Set<String> keys = new HashSet<>();
        for (String key : calendarGroups) {
          keys.add(buildGroupCalendarQuery(key));
        }
        eXoGroupCalendarCache.select(new GroupCalendarSelector(keys));
      }

      groupCalendarEventCache.select(new GroupCalendarEventSelector(calendarNode.getPath()));
      groupCalendarRecurrentEventCache.select(new GroupCalendarEventSelector(calendarNode.getPath()));

      return event;
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public CalendarEvent getEvent(Node eventNode) throws Exception {
    CalendarEvent event = new CalendarEvent();
    event = EventPageListQuery.getEventFromNode(event,
                                                eventNode,
                                                getReminderFolder(eventNode.getProperty(Utils.EXO_FROM_DATE_TIME)
                                                                  .getDate()
                                                                  .getTime(), false));
    StringBuilder namePattern = new StringBuilder(128);
    namePattern.append(Utils.EXO_RECURRENCE_ID).append('|').append(Utils.EXO_IS_EXCEPTION).append('|').append(Utils.EXO_REPEAT_UNTIL)
    .append('|').append(Utils.EXO_REPEAT_COUNT).append('|').append(Utils.EXO_ORIGINAL_REFERENCE).append('|')
    .append(Utils.EXO_REPEAT_INTERVAL).append('|').append(Utils.EXO_EXCLUDE_ID).append('|').append(Utils.EXO_REPEAT_BYDAY)
    .append('|').append(Utils.EXO_REPEAT_BYMONTHDAY);
    PropertyIterator it = eventNode.getProperties(namePattern.toString());
    while (it.hasNext()) {
      Property p = it.nextProperty();
      String name = p.getName();
      if (name.equals(Utils.EXO_RECURRENCE_ID)) {
        event.setRecurrenceId(p.getString());
      } else if (name.equals(Utils.EXO_IS_EXCEPTION)) {
        event.setIsExceptionOccurrence(p.getBoolean());
      } else if (name.equals(Utils.EXO_REPEAT_UNTIL)) {
        event.setRepeatUntilDate(p.getDate().getTime());
      } else if (name.equals(Utils.EXO_REPEAT_COUNT)) {
        event.setRepeatCount(p.getLong());
      } else if (name.equals(Utils.EXO_ORIGINAL_REFERENCE)) {
        event.setOriginalReference(p.getString());
      } else if (name.equals(Utils.EXO_REPEAT_INTERVAL)) {
        event.setRepeatInterval(p.getLong());
      } else if (name.equals(Utils.EXO_EXCLUDE_ID)) {
        Value[] values = p.getValues();
        if (values.length == 1) {
          event.setExcludeId(new String[] { values[0].getString() });
        } else {
          String[] excludeIds = new String[values.length];
          for (int i = 0; i < values.length; i++) {
            excludeIds[i] = values[i].getString();
          }
          event.setExcludeId(excludeIds);
        }
      } else if (name.equals(Utils.EXO_REPEAT_BYDAY)) {
        Value[] values = p.getValues();
        if (values.length == 1) {
          event.setRepeatByDay(new String[] { values[0].getString() });
        } else {
          String[] byDays = new String[values.length];
          for (int i = 0; i < values.length; i++) {
            byDays[i] = values[i].getString();
          }
          event.setRepeatByDay(byDays);
        }
      } else if (name.equals(Utils.EXO_REPEAT_BYMONTHDAY)) {
        Value[] values = p.getValues();
        if (values.length == 1) {
          event.setRepeatByMonthDay(new long[] { values[0].getLong() });
        } else {
          long[] byMonthDays = new long[values.length];
          for (int i = 0; i < values.length; i++) {
            byMonthDays[i] = values[i].getLong();
          }
          event.setRepeatByMonthDay(byMonthDays);
        }
      }             
    }

    String activitiId = ActivityTypeUtils.getActivityId(eventNode) ;
    if(activitiId != null) {
      event.setActivityId(ActivityTypeUtils.getActivityId(eventNode));
    }
    return event;
  }

  /**
   * {@inheritDoc}
   */
  public void saveEvent(Node calendarNode, CalendarEvent event, Node reminderFolder, boolean isNew) throws Exception {
    Node eventNode;
    Boolean isRepeatNode = false;
    if (isNew) {
      eventNode = calendarNode.addNode(event.getId(), Utils.EXO_CALENDAR_EVENT);
      eventNode.setProperty(Utils.EXO_ID, event.getId());
      if (Utils.isRepeatEvent(event)) {
        eventNode.addMixin(Utils.EXO_REPEAT_CALENDAR_EVENT);
        eventNode.addMixin(Utils.MIX_REFERENCEABLE);
        isRepeatNode = true;
      }

      if (Utils.isExceptionOccurrence(event)) {
        eventNode.addMixin(Utils.EXO_REPEAT_CALENDAR_EVENT);
        isRepeatNode = true;
      }
    } else {
      try {
        eventNode = calendarNode.getNode(event.getId());
        if (Utils.isRepeatEvent(event)) {
          if (!eventNode.isNodeType(Utils.EXO_REPEAT_CALENDAR_EVENT))
            eventNode.addMixin(Utils.EXO_REPEAT_CALENDAR_EVENT);
          if (!eventNode.isNodeType(Utils.MIX_REFERENCEABLE))
            eventNode.addMixin(Utils.MIX_REFERENCEABLE);
          isRepeatNode = true;
        }

      } catch (Exception e) {
        eventNode = calendarNode.addNode(event.getId(), Utils.EXO_CALENDAR_EVENT);
        eventNode.setProperty(Utils.EXO_ID, event.getId());
      }
      try {
        removeReminder(eventNode);
      } catch (Exception e) {
        if (log.isDebugEnabled())
          log.debug(e);
      }
    }

    eventNode.setProperty(Utils.EXO_SUMMARY, event.getSummary());
    eventNode.setProperty(Utils.EXO_CALENDAR_ID, event.getCalendarId());
    eventNode.setProperty(Utils.EXO_EVENT_CATEGORYID, event.getEventCategoryId());
    eventNode.setProperty(Utils.EXO_EVENT_CATEGORY_NAME, event.getEventCategoryName());
    eventNode.setProperty(Utils.EXO_DESCRIPTION, event.getDescription());
    eventNode.setProperty(Utils.EXO_LOCATION, event.getLocation());
    eventNode.setProperty(Utils.EXO_TASK_DELEGATOR, event.getTaskDelegator());

    GregorianCalendar startTime = Utils.getInstanceTempCalendar();
    GregorianCalendar endTime = Utils.getInstanceTempCalendar();
    if (event.getFromDateTime() !=null) {
      startTime.setTime(event.getFromDateTime());
    }
    eventNode.setProperty(Utils.EXO_FROM_DATE_TIME, startTime);
    eventNode.getSession().save();
    if (event.getToDateTime() != null) {
      endTime.setTime(event.getToDateTime());
    }
    eventNode.setProperty(Utils.EXO_TO_DATE_TIME, endTime);
    eventNode.getSession().save();
    eventNode.setProperty(Utils.EXO_EVENT_TYPE, event.getEventType());
    eventNode.setProperty(Utils.EXO_REPEAT, event.getRepeatType());
    eventNode.setProperty(Utils.EXO_PRIORITY, event.getPriority());
    eventNode.setProperty(Utils.EXO_IS_PRIVATE, event.isPrivate());
    eventNode.setProperty(Utils.EXO_EVENT_STATE, event.getEventState());
    if (event.getInvitation() == null)
      event.setInvitation(new String[] {});
    eventNode.setProperty(Utils.EXO_INVITATION, event.getInvitation());
    if (event.getParticipant() == null)
      event.setParticipant(new String[] {});
    eventNode.setProperty(Utils.EXO_PARTICIPANT, event.getParticipant());
    List<Reminder> reminders = event.getReminders();
    if (reminders != null && !reminders.isEmpty()) {
      for (Reminder rm : reminders) {
        rm.setFromDateTime(event.getFromDateTime());
        addReminder(eventNode, reminderFolder, rm);
      }
    }
    if (eventNode.hasNode(Utils.ATTACHMENT_NODE)) {
      while (eventNode.getNodes().hasNext()) {
        eventNode.getNodes().nextNode().remove();
      }
      eventNode.save();
    }
    List<Attachment> attachments = event.getAttachment();
    if (attachments != null) {
      for (Attachment att : attachments) {
        addAttachment(eventNode, att, isNew);
      }
    }

    eventNode.setProperty(Utils.EXO_MESSAGE, event.getMessage());
    eventNode.setProperty(Utils.EXO_SEND_OPTION, event.getSendOption());
    if (event.getParticipantStatus() == null)
      event.setParticipantStatus(new String[] {});
    eventNode.setProperty(Utils.EXO_PARTICIPANT_STATUS, event.getParticipantStatus());

    if (isRepeatNode) {
      if (event.getRecurrenceId() != null)
        eventNode.setProperty(Utils.EXO_RECURRENCE_ID, event.getRecurrenceId());
      else
        eventNode.setProperty(Utils.EXO_RECURRENCE_ID, Utils.EMPTY_STR);

      if (event.getIsExceptionOccurrence() != null)
        eventNode.setProperty(Utils.EXO_IS_EXCEPTION, event.getIsExceptionOccurrence());
      else
        eventNode.setProperty(Utils.EXO_IS_EXCEPTION, (Value) null);

      if (event.getExcludeId() != null && event.getExcludeId().length > 0)
        eventNode.setProperty(Utils.EXO_EXCLUDE_ID, event.getExcludeId());
      else
        eventNode.setProperty(Utils.EXO_EXCLUDE_ID, (Value[]) null);

      if (event.getRepeatUntilDate() != null) {
        GregorianCalendar dateRepeatTo = Utils.getInstanceTempCalendar();
        dateRepeatTo.setTime(event.getRepeatUntilDate());
        eventNode.setProperty(Utils.EXO_REPEAT_UNTIL, dateRepeatTo);
      } else {
        eventNode.setProperty(Utils.EXO_REPEAT_UNTIL, (Value) null);
      }

      if (event.getOriginalReference() != null)
        eventNode.setProperty(Utils.EXO_ORIGINAL_REFERENCE, event.getOriginalReference());
      else
        eventNode.setProperty(Utils.EXO_ORIGINAL_REFERENCE, (Value) null);

      eventNode.setProperty(Utils.EXO_REPEAT_COUNT, event.getRepeatCount());
      eventNode.setProperty(Utils.EXO_REPEAT_INTERVAL, event.getRepeatInterval());

      if (event.getRepeatByDay() != null)
        eventNode.setProperty(Utils.EXO_REPEAT_BYDAY, event.getRepeatByDay());
      else
        eventNode.setProperty(Utils.EXO_REPEAT_BYDAY, (Value[]) null);

      if (event.getRepeatByMonthDay() != null) {
        long[] monthDays = event.getRepeatByMonthDay();
        String[] byMonthDay = new String[monthDays.length];
        for (int i = 0; i < monthDays.length; i++)
          byMonthDay[i] = String.valueOf(monthDays[i]);
        eventNode.setProperty(Utils.EXO_REPEAT_BYMONTHDAY, byMonthDay);
      } else {
        eventNode.setProperty(Utils.EXO_REPEAT_BYMONTHDAY, (Value[]) null);
      }

      // if event is original repeating event, calculate the end-date to save
      String repeatType = event.getRepeatType();
      if (!Utils.isEmpty(repeatType) && !repeatType.equals(CalendarEvent.RP_NOREPEAT)) {
        Date repeatFinish = calculateRecurrenceFinishDate(event);
        if (repeatFinish != null) {
          GregorianCalendar finishRepeatDate = Utils.getInstanceTempCalendar();
          finishRepeatDate.setTime(repeatFinish);
          eventNode.setProperty(Utils.EXO_REPEAT_FINISH_DATE, finishRepeatDate);
        }
      }
    }
    if(event.getActivityId() != null) {
      ActivityTypeUtils.attachActivityId(eventNode, event.getActivityId());
    }
    calendarNode.getSession().save();

    // only need for event, because we just get free/busy time of events in schedule tab.
    if(CalendarEvent.TYPE_EVENT.equals(event.getEventType())) {
      addEvent(event);
    }

    Calendar calendar = loadCalendar(calendarNode);
    String[] calendarGroups = calendar.getGroups();
    if (calendarGroups != null) {
      Set<String> keys = new HashSet<>();
      for (String key : calendarGroups) {
        keys.add(buildGroupCalendarQuery(key));
      }

      eXoGroupCalendarCache.select(new GroupCalendarSelector(keys));
    }

    eventCache.remove(event.getId());
  }

  /**
   * {@inheritDoc}
   */
  public void addReminder(Node eventNode, Node reminderFolder, Reminder reminder) throws Exception {
    Node reminderNode;
    Node catNode;
    try {
      catNode = reminderFolder.getNode(eventNode.getName());
    } catch (Exception e) {
      catNode = reminderFolder.addNode(eventNode.getName(), Utils.NT_UNSTRUCTURED);
    }
    try {
      reminderNode = catNode.getNode(reminder.getId());
    } catch (Exception e) {
      reminderNode = catNode.addNode(reminder.getId(), Utils.EXO_REMINDER);
    }
    reminderNode.setProperty(Utils.EXO_EVENT_ID, eventNode.getName());
    reminderNode.setProperty(Utils.EXO_ALARM_BEFORE, reminder.getAlarmBefore());
    reminderNode.setProperty(Utils.EXO_TIME_INTERVAL, reminder.getRepeatInterval());
    reminderNode.setProperty(Utils.EXO_REMINDER_TYPE, reminder.getReminderType());
    reminderNode.setProperty(Utils.EXO_EMAIL, reminder.getEmailAddress());
    reminderNode.setProperty(Utils.EXO_IS_REPEAT, reminder.isRepeat());
    reminderNode.setProperty(Utils.EXO_IS_OVER, false);
    if (reminder.getReminderType().equals(Reminder.TYPE_POPUP)) {
      reminderNode.setProperty(Utils.EXO_OWNER, reminder.getReminderOwner());
    }
    java.util.Calendar fromTime = new GregorianCalendar();
    java.util.Calendar remindTime = new GregorianCalendar();
    java.util.Calendar toTime = new GregorianCalendar();

    if (reminder.getFromDateTime() != null) {
      fromTime.setTime(reminder.getFromDateTime());
      reminderNode.setProperty(Utils.EXO_FROM_DATE_TIME, fromTime);
      long time = reminder.getFromDateTime().getTime() - (reminder.getAlarmBefore() * 60 * 1000);
      remindTime.setTimeInMillis(time);
      reminderNode.setProperty(Utils.EXO_REMINDER_DATE, remindTime);
    }

    reminderNode.setProperty(Utils.EXO_DESCRIPTION, "");
    reminderNode.setProperty(Utils.EXO_SUMMARY, eventNode.getProperty(Utils.EXO_SUMMARY)
                             .getString());
    if (!reminderFolder.isNew())
      reminderFolder.save();
    else
      reminderFolder.getSession().save();
    
    eventCache.remove(eventNode.getProperty(Utils.EXO_ID).getString());
  }

  /**
   * creates a virtual event for a new event in a public folder of storage
   * this virtual event is search-able (by XPATH query) and used to find the free/busy time in Schedule tab 
   */
  public void addEvent(CalendarEvent event) throws Exception {
    Node eventFolder = getEventFolder(event.getFromDateTime());
    Node publicEvent;
    int fromDate;
    int toDate;
    syncRemoveEvent(eventFolder, event.getId());
    CalendarEvent ev = new CalendarEvent();
    publicEvent = eventFolder.addNode(ev.getId(), Utils.EXO_CALENDAR_PUBLIC_EVENT);
    publicEvent.setProperty(Utils.EXO_ID, ev.getId());
    publicEvent.setProperty(Utils.EXO_ROOT_EVENT_ID, event.getId());
    publicEvent.setProperty(Utils.EXO_EVENT_TYPE, event.getEventType());
    publicEvent.setProperty(Utils.EXO_CALENDAR_ID, event.getCalendarId());
    java.util.Calendar startTime = Utils.getInstanceTempCalendar();
    java.util.Calendar endTime = Utils.getInstanceTempCalendar();

    startTime.setTime(event.getFromDateTime());
    fromDate = startTime.get(java.util.Calendar.DAY_OF_YEAR);
    publicEvent.setProperty(Utils.EXO_FROM_DATE_TIME, startTime);
    publicEvent.getSession().save();
    publicEvent.setProperty(Utils.EXO_EVENT_STATE, event.getEventState());
    endTime.setTime(event.getToDateTime());
    toDate = endTime.get(java.util.Calendar.DAY_OF_YEAR);
    if (toDate > fromDate) {
      java.util.Calendar tmpTime = Utils.getInstanceTempCalendar();
      tmpTime.setTime(event.getFromDateTime());
      tmpTime.set(java.util.Calendar.HOUR_OF_DAY, 0);
      tmpTime.set(java.util.Calendar.MINUTE, 0);
      tmpTime.set(java.util.Calendar.SECOND, 0);
      tmpTime.set(java.util.Calendar.MILLISECOND, 0);
      tmpTime.setTimeInMillis(tmpTime.getTimeInMillis() + (24 * 60 * 60 * 1000) - 1000);
      publicEvent.setProperty(Utils.EXO_TO_DATE_TIME, tmpTime);
      publicEvent.getSession().save();
    } else {
      publicEvent.setProperty(Utils.EXO_TO_DATE_TIME, endTime);
      publicEvent.getSession().save();
    }
    publicEvent.setProperty(Utils.EXO_PARTICIPANT, event.getParticipant());
    try {
      if (!eventFolder.isNew())
        eventFolder.save();
      else
        eventFolder.getSession().save();
    } catch (Exception e) {
      eventFolder.getSession().refresh(true);
      eventFolder.getSession().save();
    }
    try {
      if (toDate > fromDate) {
        java.util.Calendar cal = Utils.getInstanceTempCalendar();
        cal.setTime(event.getFromDateTime());
        cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
        cal.set(java.util.Calendar.MINUTE, 0);
        cal.set(java.util.Calendar.SECOND, 0);
        cal.set(java.util.Calendar.MILLISECOND, 0);
        for (int i = fromDate + 1; i <= toDate; i++) {
          cal.roll(java.util.Calendar.DAY_OF_YEAR, true);
          Node dateFolder = getEventFolder(cal.getTime());
          ev = new CalendarEvent();
          eventFolder.getSession()
          .getWorkspace()
          .copy(publicEvent.getPath(), dateFolder.getPath() + Utils.SLASH + ev.getId());
          dateFolder.getSession().save();
          if (i <= toDate) {
            Node newEvent = dateFolder.getNode(ev.getId());
            newEvent.setProperty(Utils.EXO_ID, ev.getId());
            newEvent.setProperty(Utils.EXO_FROM_DATE_TIME, cal);
            newEvent.getSession().save();
            java.util.Calendar tmpCal = Utils.getInstanceTempCalendar();
            if (i == toDate)
              tmpCal.setTime(event.getToDateTime());
            else
              tmpCal.setTimeInMillis(cal.getTimeInMillis() + (24 * 60 * 60 * 1000) - 1000);
            newEvent.setProperty(Utils.EXO_TO_DATE_TIME, tmpCal);
            newEvent.getSession().save();
          }
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void syncRemoveEvent(Node eventFolder, String rootEventId) throws Exception {
    QueryManager qm = eventFolder.getSession().getWorkspace().getQueryManager();
    StringBuilder queryString = new StringBuilder("/jcr:root")
    .append(eventFolder.getParent().getParent().getParent().getPath())
    .append("//element(*,exo:calendarPublicEvent)[@exo:rootEventId='").append(rootEventId)
    .append("']");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator it = result.getNodes();
    while (it.hasNext()) {
      it.nextNode().remove();
    }
    eventCache.remove(rootEventId);
    eventFolder.getSession().save();
    eventFolder.refresh(true);
  }
  
  public Node getReminderFolder(Date fromDate) throws Exception {
    return getReminderFolder(fromDate, true);
  }

  /**
   * {@inheritDoc}
   */
  public Node getReminderFolder(Date fromDate, boolean create) throws Exception {
    Node publicApp = getPublicCalendarServiceHome();
    Node dateFolder = getDateFolder(publicApp, fromDate);
    try {
      return dateFolder.getNode(Utils.CALENDAR_REMINDER);
    } catch (PathNotFoundException pnfe) {
      if (create) {
        Lock lock = getLock("RemiderFolder", dateFolder.getPath());
        lock.lock();
        try {
          if (!dateFolder.hasNode(Utils.CALENDAR_REMINDER)) {
            dateFolder.addNode(Utils.CALENDAR_REMINDER, Utils.NT_UNSTRUCTURED);
            if (dateFolder.isNew())
              dateFolder.getSession().save();
            else
              dateFolder.save();
          }
        } catch (Exception e) {
          dateFolder.refresh(false);
        } finally {
          lock.unlock();
        }        
      } else {
        return null;
      }
      
      return dateFolder.getNode(Utils.CALENDAR_REMINDER);
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getEventFolder(Date fromDate) throws Exception {
    Node publicApp = getPublicCalendarServiceHome();
    Node dateFolder = getDateFolder(publicApp, fromDate);
    try {
      return dateFolder.getNode(CALENDAR_EVENT);
    } catch (PathNotFoundException e) {
      dateFolder.addNode(CALENDAR_EVENT, Utils.NT_UNSTRUCTURED);
      getPublicRoot().getSession().save();
      return dateFolder.getNode(CALENDAR_EVENT);
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getDateFolder(Node publicApp, Date date) throws Exception {
    if (date instanceof DateTime) {
      date = new Date(date.getTime());
    }
    java.util.Calendar fromCalendar = Utils.getInstanceTempCalendar();
    fromCalendar.setTime(date);
    Node yearNode;
    Node monthNode;
    String year = "Y" + String.valueOf(fromCalendar.get(java.util.Calendar.YEAR));
    String month = "M" + String.valueOf(fromCalendar.get(java.util.Calendar.MONTH) + 1);
    String day = "D" + String.valueOf(fromCalendar.get(java.util.Calendar.DATE));
    try {
      yearNode = publicApp.getNode(year);
    } catch (PathNotFoundException e) {
      yearNode = publicApp.addNode(year, Utils.NT_UNSTRUCTURED);
    }
    try {
      monthNode = yearNode.getNode(month);
    } catch (PathNotFoundException e) {
      monthNode = yearNode.addNode(month, Utils.NT_UNSTRUCTURED);
    }
    try {
      return monthNode.getNode(day);
    } catch (PathNotFoundException e) {
      return monthNode.addNode(day, Utils.NT_UNSTRUCTURED);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void addAttachment(Node eventNode, Attachment attachment, boolean isNew) throws Exception {
    Node attachHome;
    Node attachNode;
    // fix load image on IE6 UI
    ExtendedNode extNode = (ExtendedNode) eventNode;
    if (extNode.canAddMixin("exo:privilegeable"))
      extNode.addMixin("exo:privilegeable");
    String[] arrayPers = { PermissionType.READ, PermissionType.ADD_NODE,
        PermissionType.SET_PROPERTY, PermissionType.REMOVE };
    extNode.setPermission(IdentityConstants.ANY, arrayPers);
    List<AccessControlEntry> permsList = extNode.getACL().getPermissionEntries();
    for (AccessControlEntry accessControlEntry : permsList) {
      extNode.setPermission(accessControlEntry.getIdentity(), arrayPers);
    }
    try {
      attachHome = eventNode.getNode(Utils.ATTACHMENT_NODE);
    } catch (Exception e) {
      attachHome = eventNode.addNode(Utils.ATTACHMENT_NODE, Utils.NT_UNSTRUCTURED);
    }
    String name = attachment.getId().substring(attachment.getId().lastIndexOf(Utils.SLASH) + 1);
    try {
      attachNode = attachHome.getNode(name);
    } catch (Exception e) {
      attachNode = attachHome.addNode(name, Utils.EXO_EVEN_TATTACHMENT);
    }
    attachNode.setProperty(Utils.EXO_FILE_NAME, attachment.getName());
    Node nodeContent = null;
    try {
      nodeContent = attachNode.getNode(Utils.JCR_CONTENT);
    } catch (Exception e) {
      nodeContent = attachNode.addNode(Utils.JCR_CONTENT, Utils.NT_RESOURCE);
    }
    nodeContent.setProperty(Utils.JCR_LASTMODIFIED, java.util.Calendar.getInstance()
                            .getTimeInMillis());
    nodeContent.setProperty(Utils.JCR_MIMETYPE, attachment.getMimeType());
    nodeContent.setProperty(Utils.JCR_DATA, attachment.getInputStream());
    eventCache.remove(eventNode.getProperty(Utils.EXO_ID).getString());
  }
  
  public void removeAttachmentById(String attId) {
      try {
        Node calendarApp = Utils.getPublicServiceHome(Utils.createSystemProvider());
        Node parentNode =  calendarApp.getSession().getItem(attId).getParent();
        calendarApp.getSession().getItem(attId).remove();
        parentNode.save();
        eventCache.remove(parentNode.getParent().getProperty(Utils.EXO_ID).getString());
      } catch (Exception e) {
        if(log.isDebugEnabled()) log.debug(e.getMessage());
      }
    }

  /**
   * {@inheritDoc}
   */
  public void saveCalendarSetting(String username, CalendarSetting setting) throws Exception {
    Node calendarHome = getUserCalendarServiceHome(username);
    addCalendarSetting(calendarHome, setting);
    Session session = calendarHome.getSession();
    session.save();
    calendarSettingCache.remove(username);
  }

  private void saveCalendarSetting(CalendarSetting setting, String username) throws Exception {
    Node calendarHome = getUserCalendarServiceHome(username);
    addCalendarSetting(calendarHome, setting);
    calendarHome.save();
    calendarSettingCache.remove(username);
  }

  /**
   * {@inheritDoc}
   */
  public void addCalendarSetting(Node calendarHome, CalendarSetting setting) throws Exception {
    Node settingNode;
    try {
      settingNode = calendarHome.getNode(CALENDAR_SETTING);
    } catch (Exception e) {
      settingNode = calendarHome.addNode(CALENDAR_SETTING, Utils.EXO_CALENDAR_SETTING);
    }
    settingNode.setProperty(Utils.EXO_VIEW_TYPE, setting.getViewType());
    settingNode.setProperty(Utils.EXO_TIME_INTERVAL, setting.getTimeInterval());
    settingNode.setProperty(Utils.EXO_WEEK_START_ON, setting.getWeekStartOn());
    settingNode.setProperty(Utils.EXO_DATE_FORMAT, setting.getDateFormat());
    settingNode.setProperty(Utils.EXO_TIME_FORMAT, setting.getTimeFormat());
    settingNode.setProperty(Utils.EXO_TIMEZONE, setting.getTimeZone());
    settingNode.setProperty(Utils.EXO_IS_SHOW_WORKING_TIME, setting.isShowWorkingTime());
    if (setting.isShowWorkingTime()) {
      settingNode.setProperty(Utils.EXO_WORKING_BEGIN, setting.getWorkingTimeBegin());
      settingNode.setProperty(Utils.EXO_WORKING_END, setting.getWorkingTimeEnd());
    }
    settingNode.setProperty(Utils.EXO_BASE_URL, setting.getBaseURL());
    settingNode.setProperty(Utils.EXO_PRIVATE_CALENDARS, setting.getFilterPrivateCalendars());
    settingNode.setProperty(Utils.EXO_PUBLIC_CALENDARS, setting.getFilterPublicCalendars());
    settingNode.setProperty(Utils.EXO_SHARED_CALENDARS, setting.getFilterSharedCalendars());
    settingNode.setProperty(Utils.EXO_SHARED_CALENDAR_COLORS, setting.getSharedCalendarsColors());
    settingNode.setProperty(Utils.EXO_SEND_OPTION, setting.getSendOption());
  }

  /**
   * {@inheritDoc}
   */
  public CalendarSetting getCalendarSetting(String username) throws Exception {
    CalendarSetting calendarSetting = calendarSettingCache.get(this, username);      
    return calendarSetting;
  }

  /**
   * @param calendarHome
   * @return
   * @throws Exception
   */
  private CalendarSetting getCalendarSetting(Node calendarHome) throws Exception {
    if (calendarHome.hasNode(CALENDAR_SETTING)) {
      CalendarSetting calendarSetting = new CalendarSetting();
      Node settingNode = calendarHome.getNode(CALENDAR_SETTING);
      StringBuilder namePattern = new StringBuilder(256);
      namePattern.append(Utils.EXO_VIEW_TYPE).append('|').append(Utils.EXO_TIME_INTERVAL).append('|').append(Utils.EXO_WEEK_START_ON)
      .append('|').append(Utils.EXO_DATE_FORMAT).append('|').append(Utils.EXO_TIME_FORMAT).append('|')
      .append(Utils.EXO_SEND_OPTION).append('|').append(Utils.EXO_BASE_URL).append('|')
      .append(Utils.EXO_TIMEZONE).append('|')
      .append(Utils.EXO_IS_SHOW_WORKING_TIME).append('|').append(Utils.EXO_WORKING_BEGIN).append('|').append(Utils.EXO_WORKING_END)
      .append('|').append(Utils.EXO_PRIVATE_CALENDARS).append('|').append(Utils.EXO_PUBLIC_CALENDARS)
      .append('|').append(Utils.EXO_SHARED_CALENDARS).append('|').append(Utils.EXO_SHARED_CALENDAR_COLORS);
      PropertyIterator it = settingNode.getProperties(namePattern.toString());
      String workingTimeBegin = null, workingTimeEnd = null;
      while (it.hasNext()) {
        Property p = it.nextProperty();
        String name = p.getName();
        if (name.equals(Utils.EXO_VIEW_TYPE)) {
          calendarSetting.setViewType(p.getString());
        } else if (name.equals(Utils.EXO_TIME_INTERVAL)) {
          calendarSetting.setTimeInterval(p.getLong());
        } else if (name.equals(Utils.EXO_WEEK_START_ON)) {
          calendarSetting.setWeekStartOn(p.getString());
        } else if (name.equals(Utils.EXO_DATE_FORMAT)) {
          calendarSetting.setDateFormat(p.getString());
        } else if (name.equals(Utils.EXO_TIME_FORMAT)) {
          calendarSetting.setTimeFormat(p.getString());
        } else if (name.equals(Utils.EXO_SEND_OPTION)) {
          calendarSetting.setSendOption(p.getString());
        } else if (name.equals(Utils.EXO_BASE_URL)) {
          calendarSetting.setBaseURL(p.getString());
        } else if (name.equals(Utils.EXO_TIMEZONE)) {
          calendarSetting.setTimeZone(p.getString());
        } else if (name.equals(Utils.EXO_IS_SHOW_WORKING_TIME)) {
          calendarSetting.setShowWorkingTime(p.getBoolean());
        } else if (name.equals(Utils.EXO_WORKING_BEGIN)) {
          workingTimeBegin = p.getString();
        } else if (name.equals(Utils.EXO_WORKING_END)) {
          workingTimeEnd = p.getString();
        } else if (name.equals(Utils.EXO_PRIVATE_CALENDARS)) {
          Value[] values = p.getValues();
          String[] calendars = new String[values.length];
          for (int i = 0; i < values.length; i++) {
            calendars[i] = values[i].getString();
          }
          calendarSetting.setFilterPrivateCalendars(calendars);
        } else if (name.equals(Utils.EXO_PUBLIC_CALENDARS)) {
          Value[] values = p.getValues();
          String[] calendars = new String[values.length];
          for (int i = 0; i < values.length; i++) {
            calendars[i] = values[i].getString();
          }
          calendarSetting.setFilterPublicCalendars(calendars);
        } else if (name.equals(Utils.EXO_SHARED_CALENDARS)) {
          Value[] values = p.getValues();
          String[] calendars = new String[values.length];
          for (int i = 0; i < values.length; i++) {
            calendars[i] = values[i].getString();
          }
          calendarSetting.setFilterSharedCalendars(calendars);
        } else if (name.equals(Utils.EXO_SHARED_CALENDAR_COLORS)) {
          Value[] values = p.getValues();
          String[] calendarsColors = new String[values.length];
          for (int i = 0; i < values.length; i++) {
            calendarsColors[i] = values[i].getString();
          }
          calendarSetting.setSharedCalendarsColors(calendarsColors);
        }
      }
      if (calendarSetting.isShowWorkingTime()) {
        calendarSetting.setWorkingTimeBegin(workingTimeBegin);
        calendarSetting.setWorkingTimeEnd(workingTimeEnd);
      }

      return calendarSetting;
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public void storeXML(String feedXML, Node rssHome, RssData rssData) throws Exception {
    Node rss;
    String rssNodeName = rssData.getName();
    if (rssHome.hasNode(rssNodeName))
      rss = rssHome.getNode(rssNodeName);
    else
      rss = rssHome.addNode(rssNodeName, Utils.EXO_RSS_DATA);
    rss.setProperty(Utils.EXO_BASE_URL, rssData.getUrl());
    rss.setProperty(Utils.EXO_TITLE, rssData.getTitle());
    rss.setProperty(Utils.EXO_CONTENT, new ByteArrayInputStream(feedXML.getBytes()));
  }


  /**
   * {@inheritDoc}
   */
  @SuppressWarnings("unchecked")
  public void removeFeed(String username, String calendarId) throws Exception {
    try {
      Node rssHome = getRssHome(username);
      NodeIterator iter = rssHome.getNodes();
      List<String> removedFeedNodes = new ArrayList<String>();
      while (iter.hasNext()) {
        Node feedNode = iter.nextNode();
        if (feedNode.isNodeType(Utils.EXO_RSS_DATA)) {
          FeedData feedData = new FeedData();
          feedData.setTitle(feedNode.getProperty("exo:title").getString());
          StringBuilder url = new StringBuilder(feedNode.getProperty(Utils.EXO_BASE_URL).getString());
          url.append("/").append(PortalContainer.getCurrentPortalContainerName());
          url.append("/").append(feedNode.getSession().getWorkspace().getName());
          url.append("/").append(username);
          url.append("/").append(feedNode.getName());
          feedData.setUrl(url.toString());

          URL feedUrl = new URL(feedData.getUrl());
          SyndFeedInput input = new SyndFeedInput();
          SyndFeed feed = input.build(new XmlReader(feedUrl));

          List entries = feed.getEntries();
          List<SyndEntry> listBefore = new ArrayList<SyndEntry>();
          listBefore.addAll(entries);
          for (int i = 0; i < listBefore.size(); i++) {
            SyndEntry entry = listBefore.get(i);
            String id = entry.getLink().substring(entry.getLink().lastIndexOf("/") + 1);
            if (id.contains(calendarId)) {
              listBefore.remove(i);
              i--;
            }
          }
          if (listBefore.size() == 0) {
            removedFeedNodes.add(feedNode.getName());
          } else {
            feed.setEntries(listBefore);
            SyndFeedOutput output = new SyndFeedOutput();
            String feedXML = output.outputString(feed);
            feedXML = StringUtils.replace(feedXML, "&amp;", "&");
            feedNode.setProperty(Utils.EXO_CONTENT, new ByteArrayInputStream(feedXML.getBytes()));
            feedNode.save();
          }
        }
      }
      if (removedFeedNodes.size() > 0) {
        for (String s : removedFeedNodes) {
          if (rssHome.getNode(s) != null) {
            rssHome.getNode(s).remove();
          }
        }
        rssHome.getSession().save();
      }

      // remove calDav
      if (rssHome.hasNode(Utils.CALDAV_NODE)) {
        iter = rssHome.getNode(Utils.CALDAV_NODE).getNodes();
        while (iter.hasNext()) {
          Node rssCal = iter.nextNode();
          if (rssCal.getPath().contains(calendarId)) {
            rssCal.remove();
          }
        }
      }
      // remove RSS
      if (rssHome.hasNode(Utils.RSS_NODE)) {
        iter = rssHome.getNode(Utils.RSS_NODE).getNodes();
        while (iter.hasNext()) {
          Node rssCal = iter.nextNode();
          if (rssCal.getPath().contains(calendarId)) {
            rssCal.remove();
          }
        }
      }
      rssHome.getSession().save();
    } catch (FileNotFoundException e) {
      if(log.isDebugEnabled()) log.debug(e.getMessage());
    }
  }

  /**
   * @param username
   * @param title
   */
  public void removeFeedData(String username, String title) {
    try {
      Node rssHome = getRssHome(username);
      NodeIterator iter = rssHome.getNodes();
      while (iter.hasNext()) {
        Node feedNode = iter.nextNode();
        if (feedNode.isNodeType(Utils.EXO_RSS_DATA)) {
          if (feedNode.getProperty(Utils.EXO_TITLE).getString().equals(title)) {
            feedNode.remove();
            break;
          }
        }
      }
      rssHome.getSession().save();
    } catch (Exception ex) {
      if (log.isDebugEnabled()) {
        log.debug("Fail to remove feed data", ex);
      }
    }

  }

  /**
   * 
   */
  public List<FeedData> getFeeds(String username) throws Exception {
    List<FeedData> feeds = new ArrayList<FeedData>();
    try {
      Node rssHome = getRssHome(username);
      NodeIterator iter = rssHome.getNodes();
      while (iter.hasNext()) {
        Node feedNode = iter.nextNode();
        if (feedNode.isNodeType(Utils.EXO_RSS_DATA)) {
          FeedData feed = new FeedData();
          feed.setTitle(feedNode.getProperty(Utils.EXO_TITLE).getString());
          StringBuilder url = new StringBuilder(feedNode.getProperty(Utils.EXO_BASE_URL).getString());

          feed.setUrl(url.toString());

          feed.setContent(feedNode.getProperty(Utils.EXO_CONTENT).getStream());

          feeds.add(feed);
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
    }
    return feeds;
  }

  /**
   * {@inheritDoc}
   */
  public int generateRss(String username,
                         List<String> calendarIds,
                         RssData rssData,
                         CalendarImportExport importExport) throws Exception {

    Node rssHomeNode = getRssHome(username);
    Node iCalHome = null;
    try {
      iCalHome = rssHomeNode.getNode(Utils.RSS_NODE);
    } catch (Exception e) {
      iCalHome = rssHomeNode.addNode(Utils.RSS_NODE, Utils.NT_UNSTRUCTURED);
    }
    try {
      SyndFeed feed = new SyndFeedImpl();
      feed.setFeedType(rssData.getVersion());
      feed.setTitle(rssData.getTitle());
      feed.setLink(rssData.getLink());
      feed.setDescription(rssData.getDescription());
      List<SyndEntry> entries = new ArrayList<SyndEntry>();
      SyndEntry entry;
      SyndContent description;
      String portalName = PortalContainer.getCurrentPortalContainerName();
      for (String calendarId : calendarIds) {
        OutputStream out = importExport.exportCalendar(username,
                                                       Arrays.asList(new String[] { calendarId }),
                                                       "0",
                                                       -1);
        if (out != null) {
          ByteArrayInputStream is = new ByteArrayInputStream(out.toString().getBytes());
          try {
            iCalHome.getNode(calendarId + Utils.ICS_EXT).setProperty(Utils.EXO_DATA, is);
          } catch (Exception e) {
            Node ical = iCalHome.addNode(calendarId + Utils.ICS_EXT, Utils.EXO_ICAL_DATA);
            ical.setProperty(Utils.EXO_DATA, is);
          }
          StringBuilder path = new StringBuilder(Utils.SLASH);
          path.append(iCalHome.getName())
          .append(Utils.SLASH)
          .append(iCalHome.getNode(calendarId + Utils.ICS_EXT).getName());
          String url = getEntryUrl(portalName,
                                   rssHomeNode.getSession().getWorkspace().getName(),
                                   username,
                                   path.toString(),
                                   rssData.getUrl());
          Calendar exoCal = getUserCalendar(username, calendarId);
          entry = new SyndEntryImpl();
          entry.setTitle(exoCal.getName());
          entry.setLink(url);
          entry.setAuthor(username);
          description = new SyndContentImpl();
          description.setType(Utils.MIMETYPE_TEXTPLAIN);
          description.setValue(exoCal.getDescription());
          entry.setDescription(description);
          entries.add(entry);
          entry.getEnclosures();
        }
      }
      if (!entries.isEmpty()) {
        feed.setEntries(entries);
        feed.setEncoding("UTF-8");
        SyndFeedOutput output = new SyndFeedOutput();
        String feedXML = output.outputString(feed);
        feedXML = StringUtils.replace(feedXML, "&amp;", "&");
        storeXML(feedXML, rssHomeNode, rssData);
        rssHomeNode.getSession().save();
      } else {
        log.info("No data to make rss!");
        return -1;
      }
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
      return -1;
    }
    return 1;
  }

  /**
   * {@inheritDoc}
   */
  public int generateRss(String username,
                         LinkedHashMap<String, Calendar> calendars,
                         RssData rssData,
                         CalendarImportExport importExport) throws Exception {
    Node rssHomeNode = getRssHome(username);
    try {
      SyndFeed feed = new SyndFeedImpl();
      feed.setFeedType(rssData.getVersion());
      feed.setTitle(rssData.getTitle());
      feed.setLink(rssData.getLink());
      feed.setDescription(rssData.getDescription());
      List<SyndEntry> entries = new ArrayList<SyndEntry>();
      SyndEntry entry;
      SyndContent description;

      // String portalName = PortalContainer.getCurrentPortalContainerName();
      for (String calendarMap : calendars.keySet()) {
        String calendarId = calendarMap.split(Utils.COLON)[1];
        String type = calendarMap.split(Utils.COLON)[0];
        OutputStream out = importExport.exportCalendar(username,
                                                       Arrays.asList(new String[] { calendarId }),
                                                       type,
                                                       -1);
        if (out != null) {
          Calendar exoCal = calendars.get(calendarMap);
          entry = new SyndEntryImpl();
          entry.setTitle(exoCal.getName());

          entry.setLink(rssHomeNode.getPath() + Utils.SLASH + calendarId);
          entry.setAuthor(username);
          description = new SyndContentImpl();
          description.setType(Utils.MIMETYPE_TEXTPLAIN);
          description.setValue(exoCal.getDescription());
          entry.setDescription(description);
          entries.add(entry);
          entry.getEnclosures();
        }
      }
      if (!entries.isEmpty()) {
        feed.setEntries(entries);
        feed.setEncoding("UTF-8");
        SyndFeedOutput output = new SyndFeedOutput();
        String feedXML = output.outputString(feed);
        feedXML = StringUtils.replace(feedXML, "&amp;", "&");
        storeXML(feedXML, rssHomeNode, rssData);
        rssHomeNode.getSession().save();
      } else {
        log.info("No data to make rss!");
        return -1;
      }
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
      return -1;
    }
    return 1;
  }


  /**
   * {@inheritDoc}
   */
  public String getEntryUrl(String portalName,
                            String wsName,
                            String username,
                            String path,
                            String baseUrl) throws Exception {
    StringBuilder url = new StringBuilder(baseUrl);
    url.append(Utils.SLASH).append(portalName).append(Utils.SLASH).append(wsName);
    if (username != null)
      url.append(Utils.SLASH).append(username);
    url.append(path);
    return url.toString();
  }

  /**
   * Runnable to use within searchHightLightEvent
   */
  private class QueryHighLightEventsTask implements Runnable {
    private Query          query;
    private CountDownLatch latch;
    public  NodeIterator   iterator;
    public QueryHighLightEventsTask(Query aQuery, CountDownLatch aLatch) {
      query = aQuery;
      latch = aLatch;
    }
    @Override
    public void run() {
      try {
        iterator = query.execute().getNodes();
        latch.countDown();
      }
      catch (RepositoryException e) {
        log.debug("RepositoryException: " + e.getLocalizedMessage());
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Deprecated
  public Map<Integer, String> searchHightLightEvent(String username,
                                                    EventQuery eventQuery,
                                                    String[] publicCalendarIds) throws Exception {
    /** collection of which day that has event */
    Map<Integer, String> mapData = new HashMap<Integer, String>();
    Query query;

    try {
      CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));
      Node calendarHome          = getUserCalendarHome(username);
      Node calendarShareNode     = getSharedCalendarHome();
      Node publicCalHome         = getPublicCalendarHome();
      java.util.Calendar fromDateCal = calSetting.createCalendar(eventQuery.getFromDate()
                                                                 .getTimeInMillis());
      java.util.Calendar toDateCal   = calSetting.createCalendar(eventQuery.getToDate()
                                                                 .getTimeInMillis());
      QueryManager qm = publicCalHome.getSession().getWorkspace().getQueryManager();
      int queryPrivateEvents = (username != null && username.length() > 0) ? 1: 0;
      CountDownLatch latch   = new CountDownLatch(queryPrivateEvents + 1);
      Thread queryPrivateEventsThread;
      Thread queryPublicEventsThread;
      QueryHighLightEventsTask queryPrivateEventsTask = null;
      QueryHighLightEventsTask queryPublicEventsTask;
      StringBuilder queryStatement;

      /** private events */
      if (queryPrivateEvents == 1) {
        queryStatement = new StringBuilder("/jcr:root").append(calendarHome.getPath())
            .append("//element(*,exo:calendarEvent)")
            .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
            .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
            .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')]")            .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");

        query = qm.createQuery(queryStatement.toString(), Query.XPATH);

        queryPrivateEventsTask   = new QueryHighLightEventsTask(query, latch);
        queryPrivateEventsThread = new Thread(queryPrivateEventsTask);
        queryPrivateEventsThread.start();
      }

      /** public events */
      queryStatement = new StringBuilder("/jcr:root").append(publicCalHome.getPath())
          .append("//element(*,exo:calendarEvent)")
          .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
          .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
          .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')");

      /** query within specific calendar ids */
      if (publicCalendarIds != null && publicCalendarIds.length > 0) {
        queryStatement.append(" and (");
        for (int i = 0; i < publicCalendarIds.length; i++) {
          if (i == 0)
            queryStatement.append("@exo:calendarId = '" + publicCalendarIds[i] + "'");
          else
            queryStatement.append(" or @exo:calendarId = '" + publicCalendarIds[i] + "'");
        }
        queryStatement.append(")");
      }
      queryStatement.append("]")
      .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");

      query = qm.createQuery(queryStatement.toString(), Query.XPATH);

      queryPublicEventsTask   = new QueryHighLightEventsTask(query, latch);
      queryPublicEventsThread = new Thread(queryPublicEventsTask);
      queryPublicEventsThread.start();

      /** shared events  */
      if (calendarShareNode.hasNode(username)) {

        PropertyIterator propertyIterator = calendarShareNode.getNode(username).getReferences();
        while (propertyIterator.hasNext()) {
          try {

            Node calendar = propertyIterator.nextProperty().getParent();

            queryStatement = new StringBuilder("/jcr:root").append(calendar.getPath())
                .append("//element(*,exo:calendarEvent)")
                .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
                .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
                .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')]")
                .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");

            query = qm.createQuery(queryStatement.toString(), Query.XPATH);

            NodeIterator it = query.execute().getNodes();

            mapData = updateMap(mapData, it, fromDateCal, toDateCal, calSetting, Calendar.TYPE_SHARED);

          } catch (Exception e) {
            if (log.isDebugEnabled()) {
              log.debug("Fail to update map of shared event", e);
            }
          }
        }
      }

      /** get data from threads */
      latch.await();
      mapData = updateMap(mapData, queryPublicEventsTask.iterator, fromDateCal, toDateCal, calSetting, Calendar.TYPE_PUBLIC);
      if (queryPrivateEventsTask != null)
        mapData = updateMap(mapData, queryPrivateEventsTask.iterator, fromDateCal, toDateCal, calSetting, Calendar.TYPE_PRIVATE);

    } catch (Exception e) {
      log.info("Exception: " + e.getLocalizedMessage());
      if (log.isDebugEnabled()) {
        log.debug("Fail to search highlight event", e);
      }
    }

    return mapData;
  }

  /**
   * {@inheritDoc}
   */
  public EventPageList searchEvent(String username,
                                   EventQuery eventQuery,
                                   String[] publicCalendarIds) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    try {
      if (eventQuery.getCalendarId() == null) {
        events.addAll(getUserEvents(username, eventQuery));
        if (publicCalendarIds != null && publicCalendarIds.length > 0) {
          eventQuery.setCalendarId(publicCalendarIds);
          events.addAll(getPublicEvents(eventQuery));
          eventQuery.setCalendarId(null);
        }
        events.addAll(getSharedEvents(username, eventQuery));
      } else {
        String calFullId = eventQuery.getCalendarId()[0];
        if (calFullId.split(Utils.COLON).length > 0) {
          String[] calId = new String[] { calFullId.split(Utils.COLON)[1] };
          int type = Integer.parseInt(calFullId.split(Utils.COLON)[0]);
          eventQuery.setCalendarId(calId);
          switch (type) {
          case Calendar.TYPE_PRIVATE:
            events.addAll(getUserEvents(username, eventQuery));
            return new EventPageList(events, 10);
          case Calendar.TYPE_SHARED:
            events.addAll(getSharedEvents(username, eventQuery));
            return new EventPageList(events, 10);
          case Calendar.TYPE_PUBLIC:
            events.addAll(getPublicEvents(eventQuery));
            return new EventPageList(events, 10);
          default:
            break;
          }
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
    }
    return new EventPageList(events, 10);
  }


  public List<Map<Integer, String>> searchHightLightEventSQL(String username, EventQuery eventQuery,
                                                             String[] privateCalendars, String[] publicCalendars) throws Exception {
    Node calendarHome      = getUserCalendarHome(username);
    Node calendarShareNode = getSharedCalendarHome();
    Node publicCalHome     = getPublicCalendarHome();
    Query query;

    CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));
    java.util.Calendar fromDateCal = calSetting.createCalendar(eventQuery.getFromDate().getTimeInMillis());
    java.util.Calendar toDateCal   = calSetting.createCalendar(eventQuery.getToDate().getTimeInMillis());

    Map<Integer, String> mapData = new HashMap<Integer, String>();
    Map<Integer, String> emptyCalendarsMap = new HashMap<Integer, String>();

    /** query private calendar */
    QueryManager qm  = calendarHome.getSession().getWorkspace().getQueryManager();
    for (String calendarId : privateCalendars) {
      String calendarPath = calendarHome.getPath() + "/" + calendarId + "/%";

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT (exo:toDateTime < TIMESTAMP '" + ISO8601.format(eventQuery.getFromDate()) + "'")
          .append(" OR exo:fromDateTime > TIMESTAMP '" + ISO8601.format(eventQuery.getToDate()) + "')")
          .append(" AND NOT (jcr:mixinTypes='exo:repeatCalendarEvent' AND NOT exo:repeat='norepeat' AND exo:recurrenceId='')");

      /**
      StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(path + "/" + calendarId)
          .append("/element(*,exo:calendarEvent)")
          .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
          .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
          .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')]")
          .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");
      **/

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);

      try {
        NodeIterator iterator = query.execute().getNodes();
        if (!iterator.hasNext()) {
          emptyCalendarsMap.put(emptyCalendarsMap.size(), calendarId);
        }

        mapData = updateMap(mapData, iterator, fromDateCal, toDateCal, calSetting, Calendar.TYPE_PRIVATE);

      } catch (RepositoryException e) {
        log.debug("RepositoryException: " + e.getLocalizedMessage());
      } catch (Exception e) {
        log.debug("Exception: " + e.getLocalizedMessage());
      }

    }

    /** if group calendar then check event cache before query */
    qm  = publicCalHome.getSession().getWorkspace().getQueryManager();

    for (String calendarId : publicCalendars) {
      String calendarPath = publicCalHome.getPath() + "/" + calendarId + "/%";

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT (exo:toDateTime < TIMESTAMP '" + ISO8601.format(eventQuery.getFromDate()) + "'")
          .append(" OR exo:fromDateTime > TIMESTAMP '" + ISO8601.format(eventQuery.getToDate()) + "')")
          .append(" AND NOT (jcr:mixinTypes='exo:repeatCalendarEvent' AND NOT exo:repeat='norepeat' AND exo:recurrenceId='')");

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      Lock lock = getLock("GroupCalendarEvent", queryEventsStatementSQL.toString());
      lock.lock();
      List<CalendarEvent> events;

      try {
        events = groupCalendarEventCache.get(queryEventsStatementSQL.toString());
        if (events == null) {
          query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);

          try {
            NodeIterator iterator = query.execute().getNodes();
            if (!iterator.hasNext()) {
              emptyCalendarsMap.put(emptyCalendarsMap.size(), calendarId);
            }

            events = getGroupHighLightEvents(iterator);
            groupCalendarEventCache.put(queryEventsStatementSQL.toString(), events);

          } catch (RepositoryException e) {
            log.debug("RepositoryException: " + e.getLocalizedMessage());
          } catch (Exception e) {
            log.debug("Exception: " + e.getLocalizedMessage());
          }
        }

        /** events exist in cache */
        mapData = updateMap1(mapData, events.iterator(), fromDateCal, toDateCal, calSetting, Calendar.TYPE_PUBLIC);

      } finally {
        lock.unlock();
      }
    }

    /** shared calendar */
    qm  = calendarShareNode.getSession().getWorkspace().getQueryManager();

    if (calendarShareNode.hasNode(username)) {
      PropertyIterator propertyIterator = calendarShareNode.getNode(username).getReferences();
      while (propertyIterator.hasNext()) {
        try {

          Node calendar = propertyIterator.nextProperty().getParent();
          StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
              .append(" WHERE jcr:path LIKE '").append(calendar.getPath()).append("/%'")
              .append(" AND NOT jcr:path LIKE '").append(calendar.getPath()).append("/%/%'")
              .append(" AND NOT (exo:toDateTime < TIMESTAMP '" + ISO8601.format(eventQuery.getFromDate()) + "'")
              .append(" OR exo:fromDateTime > TIMESTAMP '" + ISO8601.format(eventQuery.getToDate()) + "')")
              .append(" AND NOT (jcr:mixinTypes='exo:repeatCalendarEvent' AND NOT exo:repeat='norepeat' AND exo:recurrenceId='')");

          /**
           StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(path + "/" + calendarId)
           .append("/element(*,exo:calendarEvent)")
           .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
           .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
           .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')]")
           .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");
           **/

          /** event type */
          String eventType = eventQuery.getEventType();
          if (eventType != null && eventType.length() > 0) {
            queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
          }

          /** event category */
          if (!eventQuery.isSearchInAllCategories()) {
            String[] categoryIds = eventQuery.getCategoryId();
            queryEventsStatementSQL.append(" AND (");
            for (int i = 0; i < categoryIds.length; i++) {
              if (i == 0)
                queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
              else
                queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
            }
            queryEventsStatementSQL.append(")");
          }

          query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
          NodeIterator iterator = query.execute().getNodes();
          mapData = updateMap(mapData, iterator, fromDateCal, toDateCal, calSetting, Calendar.TYPE_SHARED);
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Fail to update map of shared event", e);
          }
        }
      }
    }

    List<Map<Integer, String>> result = new ArrayList<Map<Integer, String>>();
    result.add(0, mapData);
    result.add(1, emptyCalendarsMap);
    return result;
  }


  private List<CalendarEvent> getGroupHighLightEvents(NodeIterator nodeIterator) throws Exception{
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    while (nodeIterator.hasNext()) {
      events.add(getEvent(nodeIterator.nextNode()));
    }
    return events;
  }

  public Map<Integer, String> searchHighlightRecurrenceEvent(String username,
                                                             EventQuery eventQuery,
                                                             String[] publicCalendarIds,
                                                             String timezone) throws Exception {
    Map<Integer, String> mapData = new HashMap<Integer, String>();
    int fromDayOfYear = eventQuery.getFromDate().get(java.util.Calendar.DAY_OF_YEAR);
    int toDayOfYear = eventQuery.getFromDate().get(java.util.Calendar.DAY_OF_YEAR);
    int daysOfYear = eventQuery.getFromDate().getMaximum(java.util.Calendar.DAY_OF_YEAR);
    if (eventQuery.getToDate().get(java.util.Calendar.DAY_OF_YEAR) > eventQuery.getFromDate()
        .get(java.util.Calendar.DAY_OF_YEAR)) {
      toDayOfYear = toDayOfYear + daysOfYear;
    }

    // java.util.Calendar tempCalendar = Utils.getInstanceTempCalendar();
    java.util.Calendar tempCalendar = java.util.Calendar.getInstance(DateUtils.getTimeZone(timezone));

    List<CalendarEvent> originalRecurEvents = getHighLightOriginalRecurrenceEvents(username,
                                                                                   eventQuery.getFromDate(),
                                                                                   eventQuery.getToDate(),
                                                                                   publicCalendarIds);
    boolean isVictory = false;
    if (originalRecurEvents != null && originalRecurEvents.size() > 0) {
      Iterator<CalendarEvent> recurEventsIter = originalRecurEvents.iterator();
      while (recurEventsIter.hasNext() && !isVictory) {
        CalendarEvent recurEvent = recurEventsIter.next();
        Map<String, CalendarEvent> tempMap = getOccurrenceEvents(recurEvent,
                                                                 eventQuery.getFromDate(),
                                                                 eventQuery.getToDate(),
                                                                 timezone);
        if (tempMap != null) {
          for (CalendarEvent event : tempMap.values().toArray(new CalendarEvent[0])) {
            if (isVictory)
              break;
            tempCalendar.setTime(event.getFromDateTime());
            int eventFromDayOfYear = tempCalendar.get(java.util.Calendar.DAY_OF_YEAR);
            if (tempCalendar.get(java.util.Calendar.YEAR) < eventQuery.getFromDate()
                .get(java.util.Calendar.YEAR)) {
              eventFromDayOfYear = 1;
            }

            tempCalendar.setTime(event.getToDateTime());
            int eventToDayOfYear = tempCalendar.get(java.util.Calendar.DAY_OF_YEAR);
            if (tempCalendar.get(java.util.Calendar.YEAR) > eventQuery.getToDate()
                .get(java.util.Calendar.YEAR)) {
              eventToDayOfYear = 366;
            }

            Integer begin = -1;
            Integer end = -1;
            if (fromDayOfYear >= eventFromDayOfYear) {
              begin = fromDayOfYear;
              if (toDayOfYear <= eventToDayOfYear) {
                end = toDayOfYear;
                isVictory = true;
              } else {
                end = eventToDayOfYear;
              }
            } else {
              begin = eventFromDayOfYear;
              if (toDayOfYear <= eventToDayOfYear) {
                end = toDayOfYear;
              } else {
                end = eventToDayOfYear;
              }
            }
            if (begin > 0 && end > 0)
              for (Integer i = begin; i <= end; i++)
                mapData.put(i, VALUE);
          }

        }
      }
    }
    return mapData;
  }


  public List<Map<Integer, String>> searchHighlightRecurrenceEventSQL(String username, EventQuery eventQuery, String timezone,
                                                                      String[] privateCalendars, String[] publicCalendars) throws Exception {
    Map<Integer, String> mapData = new HashMap<Integer, String>();
    int fromDayOfYear = eventQuery.getFromDate().get(java.util.Calendar.DAY_OF_YEAR);
    int toDayOfYear = eventQuery.getFromDate().get(java.util.Calendar.DAY_OF_YEAR);
    int daysOfYear = eventQuery.getFromDate().getMaximum(java.util.Calendar.DAY_OF_YEAR);
    if (eventQuery.getToDate().get(java.util.Calendar.DAY_OF_YEAR) > eventQuery.getFromDate()
        .get(java.util.Calendar.DAY_OF_YEAR)) {
      toDayOfYear = toDayOfYear + daysOfYear;
    }
    java.util.Calendar tempCalendar = java.util.Calendar.getInstance(DateUtils.getTimeZone(timezone));

    List<CalendarEvent> originalRecurEvents = searchHighLightOriginalRecurrenceEventsSQL(username,
        eventQuery.getFromDate(), eventQuery.getToDate(), eventQuery, privateCalendars, publicCalendars);

    String[] emptyCalsId = new String[]{};
    if(originalRecurEvents != null && originalRecurEvents.size() > 0) {
      CalendarEvent calEvent = originalRecurEvents.get(0);
      if (calEvent.getCalendarId().equals("-1")) {
        emptyCalsId = calEvent.getSummary().split(";");
        originalRecurEvents.remove(0);
      }
    }

    boolean isVictory = false;
    if (originalRecurEvents != null && originalRecurEvents.size() > 0) {
      Iterator<CalendarEvent> recurEventsIter = originalRecurEvents.iterator();
      while (recurEventsIter.hasNext() && !isVictory) {
        CalendarEvent recurEvent = recurEventsIter.next();
        Map<String, CalendarEvent> tempMap = getOccurrenceEvents(recurEvent,
            eventQuery.getFromDate(),
            eventQuery.getToDate(),
            timezone);
        if (tempMap != null) {
          for (CalendarEvent event : tempMap.values().toArray(new CalendarEvent[0])) {
            if (isVictory)
              break;
            tempCalendar.setTime(event.getFromDateTime());
            int eventFromDayOfYear = tempCalendar.get(java.util.Calendar.DAY_OF_YEAR);
            if (tempCalendar.get(java.util.Calendar.YEAR) < eventQuery.getFromDate()
                .get(java.util.Calendar.YEAR)) {
              eventFromDayOfYear = 1;
            }

            tempCalendar.setTime(event.getToDateTime());
            int eventToDayOfYear = tempCalendar.get(java.util.Calendar.DAY_OF_YEAR);
            if (tempCalendar.get(java.util.Calendar.YEAR) > eventQuery.getToDate()
                .get(java.util.Calendar.YEAR)) {
              eventToDayOfYear = 366;
            }

            Integer begin = -1;
            Integer end = -1;
            if (fromDayOfYear >= eventFromDayOfYear) {
              begin = fromDayOfYear;
              if (toDayOfYear <= eventToDayOfYear) {
                end = toDayOfYear;
                isVictory = true;
              } else {
                end = eventToDayOfYear;
              }
            } else {
              begin = eventFromDayOfYear;
              if (toDayOfYear <= eventToDayOfYear) {
                end = toDayOfYear;
              } else {
                end = eventToDayOfYear;
              }
            }
            if (begin > 0 && end > 0)
              for (Integer i = begin; i <= end; i++)
                mapData.put(i, VALUE);
          }

        }
      }
    }

    //return mapData;
    Map<Integer, String> emptyCalendarsMap = new HashMap<Integer, String>();
    for (String calendarId : emptyCalsId) {
      emptyCalendarsMap.put(emptyCalendarsMap.size(), calendarId);
    }

    List<Map<Integer, String>> result = new ArrayList<Map<Integer, String>>();
    result.add(0, mapData);
    result.add(1, emptyCalendarsMap);
    return result;
  }

  /**
   * put busy days of the year, which contain events, to "data" map.
   * 
   * @param data The map contains busy days of the year. Its values are
   *          optional.
   * @param it
   * @param fromDate
   * @param toDate
   * @param calendarSetting
   * @param calType
   * @return
   * @throws Exception
   */
  private Map<Integer, String> updateMap(Map<Integer, String> data, NodeIterator it,
                                         java.util.Calendar fromDate, java.util.Calendar toDate,
                                         CalendarSetting calendarSetting, int calType) throws Exception {
    String[] filterCalIds = null;
    switch (calType) {
    case Calendar.TYPE_PRIVATE:
      filterCalIds = calendarSetting.getFilterPrivateCalendars();
      break;
    case Calendar.TYPE_PUBLIC:
      filterCalIds = calendarSetting.getFilterPublicCalendars();
      break;
    case Calendar.TYPE_SHARED:
      filterCalIds = calendarSetting.getFilterSharedCalendars();
      break;
    default:
      break;
    }
    int fromDayOfYear = fromDate.get(java.util.Calendar.DAY_OF_YEAR);
    int daysOfyer = fromDate.getMaximum(java.util.Calendar.DAY_OF_YEAR);
    int toDayOfYear = toDate.get(java.util.Calendar.DAY_OF_YEAR);
    if (toDate.get(java.util.Calendar.DAY_OF_YEAR) > fromDate.get(java.util.Calendar.DAY_OF_YEAR)) {
      toDayOfYear = toDayOfYear + daysOfyer;
    }
    boolean isVictory = false;
    while (it.hasNext() && !isVictory) {
      Node eventNode = it.nextNode();
      if (filterCalIds == null
          || !Arrays.asList(filterCalIds).contains(eventNode.getProperty(Utils.EXO_CALENDAR_ID)
                                                   .getString())) {
        java.util.Calendar eventFormDate = calendarSetting.createCalendar(eventNode.getProperty(Utils.EXO_FROM_DATE_TIME)
                                                                          .getDate()
                                                                          .getTime());
        java.util.Calendar eventToDate = calendarSetting.createCalendar(eventNode.getProperty(Utils.EXO_TO_DATE_TIME)
                                                                        .getDate()
                                                                        .getTime());
        int eventFromDayOfYear = eventFormDate.get(java.util.Calendar.DAY_OF_YEAR);
        int eventToDayOfYear = eventToDate.get(java.util.Calendar.DAY_OF_YEAR);

        if (eventFormDate.get(java.util.Calendar.YEAR) < fromDate.get(java.util.Calendar.YEAR)) {
          eventFromDayOfYear = 1;
        }
        if (eventToDate.get(java.util.Calendar.YEAR) > toDate.get(java.util.Calendar.YEAR)) {
          eventToDayOfYear = 366;
        }
        Integer begin = -1;
        Integer end = -1;
        if (fromDayOfYear >= eventFromDayOfYear) {
          begin = fromDayOfYear;
          if (toDayOfYear <= eventToDayOfYear) {
            end = toDayOfYear;
            isVictory = true;
          } else {
            end = eventToDayOfYear;
          }
        } else {
          begin = eventFromDayOfYear;
          if (toDayOfYear <= eventToDayOfYear) {
            end = toDayOfYear;
          } else {
            end = eventToDayOfYear;
          }
        }
        if (begin > 0 && end > 0)
          for (Integer i = begin; i <= end; i++)
            data.put(i, VALUE);
      }
    }
    return data;
  }


  private Map<Integer, String> updateMap1(Map<Integer, String> data, Iterator<CalendarEvent> it,
                                         java.util.Calendar fromDate, java.util.Calendar toDate,
                                         CalendarSetting calendarSetting, int calType) throws Exception {
    String[] filterCalIds = null;
    switch (calType) {
      case Calendar.TYPE_PRIVATE:
        filterCalIds = calendarSetting.getFilterPrivateCalendars();
        break;
      case Calendar.TYPE_PUBLIC:
        filterCalIds = calendarSetting.getFilterPublicCalendars();
        break;
      case Calendar.TYPE_SHARED:
        filterCalIds = calendarSetting.getFilterSharedCalendars();
        break;
      default:
        break;
    }
    int fromDayOfYear = fromDate.get(java.util.Calendar.DAY_OF_YEAR);
    int daysOfyer = fromDate.getMaximum(java.util.Calendar.DAY_OF_YEAR);
    int toDayOfYear = toDate.get(java.util.Calendar.DAY_OF_YEAR);
    if (toDate.get(java.util.Calendar.DAY_OF_YEAR) > fromDate.get(java.util.Calendar.DAY_OF_YEAR)) {
      toDayOfYear = toDayOfYear + daysOfyer;
    }
    boolean isVictory = false;
    while (it.hasNext() && !isVictory) {
      CalendarEvent event = it.next();
      if (filterCalIds == null
          || !Arrays.asList(filterCalIds).contains(event.getCalendarId())) {
        java.util.Calendar eventFormDate = calendarSetting.createCalendar(event.getFromDateTime().getTime());
        java.util.Calendar eventToDate = calendarSetting.createCalendar(event.getToDateTime().getTime());

        int eventFromDayOfYear = eventFormDate.get(java.util.Calendar.DAY_OF_YEAR);
        int eventToDayOfYear = eventToDate.get(java.util.Calendar.DAY_OF_YEAR);

        if (eventFormDate.get(java.util.Calendar.YEAR) < fromDate.get(java.util.Calendar.YEAR)) {
          eventFromDayOfYear = 1;
        }
        if (eventToDate.get(java.util.Calendar.YEAR) > toDate.get(java.util.Calendar.YEAR)) {
          eventToDayOfYear = 366;
        }
        Integer begin = -1;
        Integer end = -1;
        if (fromDayOfYear >= eventFromDayOfYear) {
          begin = fromDayOfYear;
          if (toDayOfYear <= eventToDayOfYear) {
            end = toDayOfYear;
            isVictory = true;
          } else {
            end = eventToDayOfYear;
          }
        } else {
          begin = eventFromDayOfYear;
          if (toDayOfYear <= eventToDayOfYear) {
            end = toDayOfYear;
          } else {
            end = eventToDayOfYear;
          }
        }
        if (begin > 0 && end > 0)
          for (Integer i = begin; i <= end; i++)
            data.put(i, VALUE);
      }
    }
    return data;
  }


  private List<Value> calculateSharedCalendar(String user,
                                              Node calendarNode,
                                              Value[] values,
                                              List<Value> valueList,
                                              Node sharedCalendarHome) throws Exception {
    CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(user));
    if (calSetting == null)
      calSetting = new CalendarSetting();
    Map<String, String> map = new HashMap<String, String>();
    for (String key : calSetting.getSharedCalendarsColors()) {
      map.put(key.split(":")[0], key.split(":")[1]);
    }
    if (map.get(calendarNode.getProperty(Utils.EXO_ID).getString()) == null)
      map.put(calendarNode.getProperty(Utils.EXO_ID).getString(),
              calendarNode.getProperty("exo:calendarColor").getString());
    List<String> calColors = new ArrayList<String>();
    for (String key : map.keySet()) {
      calColors.add(key + ":" + map.get(key));
    }
    calSetting.setSharedCalendarsColors(calColors.toArray(new String[calColors.size()]));
    saveCalendarSetting(calSetting, user);

    Node userNode = null;
    Session systemSession = sharedCalendarHome.getSession();
    try {
      userNode = sharedCalendarHome.getNode(user);
    } catch (PathNotFoundException e) {
      userNode = sharedCalendarHome.addNode(user, Utils.NT_UNSTRUCTURED);
      if (userNode.canAddMixin(Utils.MIX_REFERENCEABLE)) {
        userNode.addMixin(Utils.MIX_REFERENCEABLE);
      }
    }
    boolean isExist = false;
    isExist = false;
    for (int i = 0; i < values.length; i++) {
      Value value = values[i];
      String uuid = value.getString();
      Node refNode = systemSession.getNodeByUUID(uuid);
      if (refNode.getPath().equals(userNode.getPath())) {
        isExist = true;
        break;
      }
    }
    if (!isExist) {
      Value value2add = calendarNode.getSession().getValueFactory().createValue(userNode);
      valueList.add(value2add);
    }
    return valueList;
  }

  /**
   * {@inheritDoc}
   */
  public void shareCalendar(String username, String calendarId, List<String> receiverUsers) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
    Value[] values = {};
    if (calendarNode.isNodeType(Utils.EXO_SHARED_MIXIN)) {
      values = calendarNode.getProperty(Utils.EXO_SHARED_ID).getValues();
    }
    List<Value> valueList = new ArrayList<Value>();
    for (int i = 0; i < values.length; i++) {
      Value value = values[i];
      valueList.add(value);
    }
    for (String user : receiverUsers) {
      valueList = calculateSharedCalendar(user, calendarNode, values, valueList, sharedCalendarHome);
    }
    if (valueList.size() > 0) {
      if (!calendarNode.isNodeType(Utils.EXO_SHARED_MIXIN)) {
        calendarNode.addMixin(Utils.EXO_SHARED_MIXIN);
      }
      calendarNode.setProperty(Utils.EXO_SHARED_ID, valueList.toArray(new Value[valueList.size()]));
      calendarNode.save();
      sharedCalendarHome.getSession().save();
      calendarNode.getSession().save();
    }
    clearCalendarCache(calendarId, username);
  }

  /**
   * {@inheritDoc}
   */
  public GroupCalendarData getSharedCalendars(String username, boolean isShowAll) throws Exception {
    Node shareCalendarHome = getSharedCalendarHome();
    if (shareCalendarHome.hasNode(username)) {
      Node sharedNode = shareCalendarHome.getNode(username);
      List<Calendar> calendars = new ArrayList<Calendar>();
      PropertyIterator iter = sharedNode.getReferences();
      String[] defaultFilterCalendars = null;
      if (username != null) {
        defaultFilterCalendars = getCalendarSetting(username).getFilterSharedCalendars();
      }
      while (iter.hasNext()) {
        try {
          Calendar cal = getCalendar(defaultFilterCalendars,
                                     null,
                                     iter.nextProperty().getParent(),
                                     isShowAll);
          if (cal != null) {
            calendars.add(cal);
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Fail to create new calendar", e);
          }
        }
      }
      if (calendars.size() > 0) {
        return new GroupCalendarData("Shared", "Shared", calendars);
      }
    }
    return null;
  }

  private Node setCalendarProperties(Node calendarNode, Calendar calendar) throws Exception {
    calendarNode.setProperty(Utils.EXO_NAME, calendar.getName());
    calendarNode.setProperty(Utils.EXO_DESCRIPTION, calendar.getDescription());
    calendarNode.setProperty(Utils.EXO_VIEW_PERMISSIONS, calendar.getViewPermission());
    calendarNode.setProperty(Utils.EXO_EDIT_PERMISSIONS, calendar.getEditPermission());
    calendarNode.setProperty(Utils.EXO_GROUPS, calendar.getGroups());
    calendarNode.setProperty(Utils.EXO_LOCALE, calendar.getLocale());
    calendarNode.setProperty(Utils.EXO_TIMEZONE, calendar.getTimeZone());
    calendarNode.setProperty(Utils.EXO_CALENDAR_COLOR, calendar.getCalendarColor());
    calendarNode.setProperty(Utils.EXO_CALENDAR_OWNER, calendar.getCalendarOwner());
    try {
      calendarNode.setProperty(Utils.EXO_PUBLIC_URL, calendar.getPublicUrl());
      calendarNode.setProperty(Utils.EXO_PRIVATE_URL, calendar.getPrivateUrl());
    } catch (ConstraintViolationException e) {
      log.debug("\n\n Need to add property definition in exo:calendar node type !", e);
    }
    return calendarNode;
  }

  /**
   * {@inheritDoc}
   */
  public void saveSharedCalendar(String username, Calendar calendar) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      String uuid = userNode.getProperty("jcr:uuid").getString();
      PropertyIterator iter = userNode.getReferences();
      Node calendarNode;
      List<Value> newValues = new ArrayList<Value>();
      while (iter.hasNext()) {
        calendarNode = iter.nextProperty().getParent();
        if (calendarNode.getProperty(Utils.EXO_ID).getString().equals(calendar.getId())) {
          Value[] values = calendarNode.getProperty(Utils.EXO_SHARED_ID).getValues();
          for (Value value : values) {
            if (!value.getString().equals(uuid)) {
              newValues.add(value);
            }
          }
          calendarNode = setCalendarProperties(calendarNode, calendar);
          CalendarSetting usCalSetting = getCalendarSetting(getUserCalendarServiceHome(username));
          Map<String, String> map = new HashMap<String, String>();
          for (String key : usCalSetting.getSharedCalendarsColors()) {
            map.put(key.split(Utils.COLON)[0], key.split(Utils.COLON)[1]);
          }
          map.put(calendar.getId(), calendar.getCalendarColor());
          List<String> calColors = new ArrayList<String>();
          for (String key : map.keySet()) {
            calColors.add(key + Utils.COLON + map.get(key));
          }
          calColors.add(calendar.getId() + Utils.COLON + calendar.getCalendarColor());
          usCalSetting.setSharedCalendarsColors(calColors.toArray(new String[calColors.size()]));
          saveCalendarSetting(usCalSetting, username);
          calendarNode.save();
          break;
        }
      }
    }
    clearCalendarCache(calendar.getId(), username);
  }

  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getSharedEvents(String username, EventQuery eventQuery) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    Node shareHome = getSharedCalendarHome();
    if (shareHome.hasNode(username)) {
      QueryManager qm = shareHome.getSession().getWorkspace().getQueryManager();
      PropertyIterator iter = getSharedCalendarHome().getNode(username).getReferences();
      CalendarEvent calEvent;
      while (iter.hasNext()) {
        try {
          Node calendar = iter.nextProperty().getParent();
          eventQuery.setCalendarPath(calendar.getPath());
          Query query = qm.createQuery(eventQuery.getQueryStatement(), eventQuery.getQueryType());
          NodeIterator it = query.execute().getNodes();
          while (it.hasNext()) {
            calEvent = getEvent(it.nextNode());
            calEvent.setCalType(String.valueOf(Calendar.TYPE_SHARED));
            events.add(calEvent);
            if (eventQuery.getLimitedItems() == it.getPosition())
              break;
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Fail to get events from calendar", e);
          }
        }
      }
    }
    return events;
  }

  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getSharedEventByCalendars(String username, List<String> calendarIds) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    if (getSharedCalendarHome().hasNode(username)) {
      PropertyIterator iter = getSharedCalendarHome().getNode(username).getReferences();
      while (iter.hasNext()) {
        try {
          Node calendar = iter.nextProperty().getParent();
          if (calendarIds.contains(calendar.getProperty(Utils.EXO_ID).getString())) {
            NodeIterator it = calendar.getNodes();
            while (it.hasNext()) {
              events.add(this.getEventById(it.nextNode().getProperty(Utils.EXO_ID).getString()));
            }
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Fail to get events from calendar", e);
          }
        }
      }
    }
    return events;
  }

  public CalendarEvent getSharedEvent(String username, String calendarId, String eventId) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      PropertyIterator iter = getSharedCalendarHome().getNode(username).getReferences();
      while (iter.hasNext()) {
        try {
          Node calendar = iter.nextProperty().getParent();
          if (!calendarId.equals(calendar.getProperty(Utils.EXO_ID).getString()))
            continue;
          if (calendar.hasNode(eventId)) {
            CalendarEvent event = getEvent(calendar.getNode(eventId));
            event.setCalType(String.valueOf(Calendar.TYPE_SHARED));
            return event;
          }
        } catch (Exception e) {
          log.error("Exception when get shared event", e);
          return null;
        }
      }
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public void saveEventToSharedCalendar(String username,
                                        String calendarId,
                                        CalendarEvent event,
                                        boolean isNew) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      PropertyIterator iter = userNode.getReferences();
      Node calendar;
      while (iter.hasNext()) {
        calendar = iter.nextProperty().getParent();
        if (calendar.getProperty(Utils.EXO_ID).getString().equals(calendarId)) {

          if (!canEdit(calendar, username)) {
            log.debug("\n Do not have edit permission. \n");
            throw new AccessDeniedException();
          }
          Node reminderFolder = getReminderFolder(event.getFromDateTime());
          saveEvent(calendar, event, reminderFolder, isNew);
          calendar.save();
          break;
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public boolean canEdit(Node calNode, String username) throws Exception {
    OrganizationService oService = (OrganizationService) ExoContainerContext.getCurrentContainer()
        .getComponentInstanceOfType(OrganizationService.class);

    Value[] editValues = calNode.getProperty(Utils.EXO_EDIT_PERMISSIONS).getValues();
    String[] editPerms = new String[editValues.length];
    for (int i = 0; i < editValues.length; i++) {
      editPerms[i] = editValues[i].getString();
    }
    return Utils.hasPermission(oService, editPerms, username);
  }

  private void clearCalendarCache(String calendarId, String username) {
    calendarCache.remove(new KeyValuePair(calendarId, username));
    if(StringUtils.isNotBlank(username)) {
      calendarCache.remove(new KeyValuePair(calendarId, null));
    }
  }

  /**
   * Runnable used within getAllNoRepeatEvents
   */
  private class QueryEventsTask implements Runnable {
    private Query query;
    private CountDownLatch latch;
    private String calendarType;
    public List<CalendarEvent> events;
    public QueryEventsTask(Query aQuery, String type, CountDownLatch aLatch) {
      query = aQuery;
      latch = aLatch;
      calendarType = type;
      events = new ArrayList<CalendarEvent>();
    }

    @Override
    public void run() {

      try {
        NodeIterator iterator = query.execute().getNodes();
        CalendarEvent event;
        while (iterator.hasNext()) {
          event = getEvent(iterator.nextNode());
          event.setCalType(String.valueOf(calendarType));
          events.add(event);
        }
        latch.countDown();
      } catch (RepositoryException e) {
        log.debug("RepositoryException: " + e.getLocalizedMessage());
      } catch (Exception e) {
        log.debug("Exception: " + e.getLocalizedMessage());
      }
    }
  }

  /**
   * We do not use this method anymore, so i mark this method as deprecated
   * to notify other team do not use this method any more and we do not need to maintain this method.
   * {@inheritDoc}
   */
  @Deprecated
  public List<CalendarEvent> getAllNoRepeatEvents(String username, EventQuery eventQuery, String[] publicCalendarIds) throws Exception {

    List<CalendarEvent> allEvents = new ArrayList<CalendarEvent>();
    String[] filteredCalendars;
    Node calendarHome      = getUserCalendarHome(username);
    Node calendarShareNode = getSharedCalendarHome();
    Node publicCalHome     = getPublicCalendarHome();
    Query query;
    CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));
    QueryManager qm = publicCalHome.getSession().getWorkspace().getQueryManager();


    CountDownLatch latch = new CountDownLatch(2);

    /** query private calendar events */
    filteredCalendars = calSetting.getFilterPrivateCalendars();
    StringBuilder queryStatement = new StringBuilder("/jcr:root").append(calendarHome.getPath())
        .append("//element(*,exo:calendarEvent)")
        .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
        .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
        .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')");

    if (filteredCalendars != null && filteredCalendars.length > 0) {
      queryStatement.append(" and (");
      for (int i = 0; i < filteredCalendars.length; i++) {
        if (i == 0)
          queryStatement.append("@exo:calendarId != '" + filteredCalendars[i] + "'");
        else
          queryStatement.append(" or @exo:calendarId != '" + filteredCalendars[i] + "'");
      }
    }

    /** event type */
    String eventType = eventQuery.getEventType();
    if (eventType != null && eventType.length() > 0) {
      queryStatement.append(" and (@exo:eventType='" + eventType + "')");
    }

    /** event category */
    String[] categoryIds = eventQuery.getCategoryId();
    if (categoryIds != null && categoryIds.length > 0) {
      queryStatement.append(" and (");
      for (int i = 0; i < categoryIds.length; i++) {
        if (i == 0)
          queryStatement.append("@exo:eventCategoryId='").append(categoryIds[i]).append("'");
        else
          queryStatement.append(" or @exo:eventCategoryId='").append(categoryIds[i]).append("'");
      }
      queryStatement.append(")");
    }

    queryStatement.append("]");
    query = qm.createQuery(queryStatement.toString(), Query.XPATH);
    QueryEventsTask queryPrivateEventsTask = new QueryEventsTask(query, String.valueOf(Calendar.TYPE_PRIVATE), latch);
    Thread queryPrivateEventsThread = new Thread(queryPrivateEventsTask);
    queryPrivateEventsThread.start();

    /** query public events */
    filteredCalendars = calSetting.getFilterPublicCalendars();
    queryStatement = new StringBuilder("/jcr:root").append(publicCalHome.getPath())
        .append("//element(*,exo:calendarEvent)")
        .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
        .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "')) and @exo:repeat='norepeat'");

    if (publicCalendarIds != null && publicCalendarIds.length > 0) {
      queryStatement.append(" and (");
      for (int i = 0; i < publicCalendarIds.length; i++) {
        if (i == 0)
          queryStatement.append("@exo:calendarId = '" + publicCalendarIds[i] + "'");
        else
          queryStatement.append(" or @exo:calendarId = '" + publicCalendarIds[i] + "'");
      }
      queryStatement.append(")");
    }

    if (filteredCalendars != null && filteredCalendars.length > 0) {
      queryStatement.append(" and (");
        for (int i = 0; i < filteredCalendars.length; i++) {
          if (i == 0)
            queryStatement.append("@exo:calendarId != '" + filteredCalendars[i] + "'");
          else
            queryStatement.append(" or @exo:calendarId != '" + filteredCalendars[i] + "'");
        }
        queryStatement.append(")");
    }

    /** event type */
    eventType = eventQuery.getEventType();
    if (eventType != null && eventType.length() > 0) {
      queryStatement.append(" and (@exo:eventType='" + eventType + "')");
    }

    /** event category */
    categoryIds = eventQuery.getCategoryId();
    if (categoryIds != null && categoryIds.length > 0) {
      queryStatement.append(" and (");
      for (int i = 0; i < categoryIds.length; i++) {
        if (i == 0)
          queryStatement.append("@exo:eventCategoryId='").append(categoryIds[i]).append("'");
        else
          queryStatement.append(" or @exo:eventCategoryId='").append(categoryIds[i]).append("'");
      }
      queryStatement.append(")");
    }
    queryStatement.append("]");

    query = qm.createQuery(queryStatement.toString(), Query.XPATH);

    QueryEventsTask queryPublicEventsTask = new QueryEventsTask(query, String.valueOf(Calendar.TYPE_PUBLIC), latch);
    Thread queryPublicEventsThread = new Thread(queryPublicEventsTask);
    queryPublicEventsThread.start();

    /** query shared events */
    filteredCalendars = calSetting.getFilterSharedCalendars();
    try {
      PropertyIterator propertyIterator = calendarShareNode.getNode(username).getReferences();
      while (propertyIterator.hasNext()) {
        Node calendar = propertyIterator.nextProperty().getParent();
        queryStatement = new StringBuilder("/jcr:root").append(calendar.getPath())
            .append("//element(*,exo:calendarEvent)")
            .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
            .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
            .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')");

        if (filteredCalendars != null && filteredCalendars.length > 0) {
          queryStatement.append(" and (");
          for (int i = 0; i < filteredCalendars.length; i++) {
            if (i == 0)
              queryStatement.append("@exo:calendarId != '" + filteredCalendars[i] + "'");
            else
              queryStatement.append(" or @exo:calendarId != '" + filteredCalendars[i] + "'");
          }
          queryStatement.append(")");
        }

        /** event type */
        eventType = eventQuery.getEventType();
        if (eventType != null && eventType.length() > 0) {
          queryStatement.append(" and (@exo:eventType='" + eventType + "')");
        }

        /** event category */
        categoryIds = eventQuery.getCategoryId();
        if (categoryIds != null && categoryIds.length > 0) {
          queryStatement.append(" and (");
          for (int i = 0; i < categoryIds.length; i++) {
            if (i == 0)
              queryStatement.append("@exo:eventCategoryId='").append(categoryIds[i]).append("'");
            else
              queryStatement.append(" or @exo:eventCategoryId='").append(categoryIds[i]).append("'");
          }
          queryStatement.append(")");
        }
        queryStatement.append("]");
        query = qm.createQuery(queryStatement.toString(), Query.XPATH);
        NodeIterator iterator = query.execute().getNodes();
        CalendarEvent event;
        while (iterator.hasNext()) {
          event = getEvent(iterator.nextNode());
          event.setCalType(String.valueOf(Calendar.TYPE_SHARED));
          allEvents.add(event);
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug("Exception: " + e.getLocalizedMessage());
      }
    }
    latch.await();
    allEvents.addAll(queryPrivateEventsTask.events);
    allEvents.addAll(queryPublicEventsTask.events);
    return allEvents;
  }

  private class QueryRecurrentEventsTask implements Runnable {
    private CountDownLatch     latch;
    private Node               homeNode;
    private String             calendarType;
    private java.util.Calendar fromDate;
    private java.util.Calendar toDate;
    private String[]           filteredCalendars;
    private String[]           calendarIds;
    public List<CalendarEvent> calendarEvents;

    public QueryRecurrentEventsTask(Node calendarHome, String type,
                                    java.util.Calendar from, java.util.Calendar to,
                                    String[] _calendarIds, String[] _filteredCalendars,
                                    CountDownLatch aLatch) {
      homeNode     = calendarHome;
      calendarType = type;
      fromDate     = from;
      toDate       = to;
      latch        = aLatch;
      filteredCalendars = _filteredCalendars;
      calendarIds  = _calendarIds;
    }

    @Override
    public void run() {

      try {
        calendarEvents = getOriginalRecurrenceEvents1(homeNode, calendarType,
                                                      fromDate, toDate, calendarIds, filteredCalendars) ;
        latch.countDown();
      } catch (Exception e) {
        if(log.isDebugEnabled()) log.debug("Exception: " + e.getLocalizedMessage());
      }
    }
  }


  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getAllNoRepeatEventsSQL(String username, EventQuery eventQuery,
                                                     String[] privateCalendars, String[] publicCalendars,
                                                     List<String> emptyCalendars) throws Exception {
    List<CalendarEvent> allEvents = new ArrayList<CalendarEvent>();

    Node calendarHome      = getUserCalendarHome(username);
    Node calendarShareNode = getSharedCalendarHome();
    Node publicCalHome     = getPublicCalendarHome();
    Query query;

    /** query private calendar */
    QueryManager qm  = calendarHome.getSession().getWorkspace().getQueryManager();

    for (String calendarId : privateCalendars) {
      if (emptyCalendars!= null && emptyCalendars.contains(calendarId)) continue;
      String calendarPath = calendarHome.getPath() + "/" + calendarId + "/%";

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT (exo:toDateTime < TIMESTAMP '" + ISO8601.format(eventQuery.getFromDate()) + "'")
          .append(" OR exo:fromDateTime > TIMESTAMP '" + ISO8601.format(eventQuery.getToDate()) + "')")
          .append(" AND NOT (jcr:mixinTypes='exo:repeatCalendarEvent' AND NOT exo:repeat='norepeat' AND exo:recurrenceId='')");

      /**
       StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(path + "/" + calendarId)
       .append("/element(*,exo:calendarEvent)")
       .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
       .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
       .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')]")
       .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");
       **/

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);

      try {
        NodeIterator iterator = query.execute().getNodes();
        CalendarEvent event;
        while (iterator.hasNext()) {
          event = getEvent(iterator.nextNode());
          event.setCalType(String.valueOf(Calendar.TYPE_PRIVATE));
          allEvents.add(event);
        }
      } catch (RepositoryException e) {
        log.debug("RepositoryException: " + e.getLocalizedMessage());
      } catch (Exception e) {
        log.debug("Exception: " + e.getLocalizedMessage());
      }

    }

    /** if group calendar then check event cache before query */
    qm  = publicCalHome.getSession().getWorkspace().getQueryManager();

    for (String calendarId : publicCalendars) {
      if (emptyCalendars!= null && emptyCalendars.contains(calendarId)) continue;
      String calendarPath = publicCalHome.getPath() + "/" + calendarId + "/%";

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT (exo:toDateTime < TIMESTAMP '" + ISO8601.format(eventQuery.getFromDate()) + "'")
          .append(" OR exo:fromDateTime > TIMESTAMP '" + ISO8601.format(eventQuery.getToDate()) + "')")
          .append(" AND NOT (jcr:mixinTypes='exo:repeatCalendarEvent' AND NOT exo:repeat='norepeat' AND exo:recurrenceId='')");

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      Lock lock = getLock("GroupCalendarEvent", queryEventsStatementSQL.toString());
      lock.lock();
      List<CalendarEvent> events;

      try {

        events = groupCalendarEventCache.get(queryEventsStatementSQL.toString());
        if (events == null) {
          query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
          events = new ArrayList<CalendarEvent>();

          try {
            NodeIterator iterator = query.execute().getNodes();
            CalendarEvent event;
            while (iterator.hasNext()) {
              event = getEvent(iterator.nextNode());
              event.setCalType(String.valueOf(Calendar.TYPE_PUBLIC));
              events.add(event);
            }

            groupCalendarEventCache.put(queryEventsStatementSQL.toString(), events);

          } catch (RepositoryException e) {
            log.debug("RepositoryException: " + e.getLocalizedMessage());
          } catch (Exception e) {
            log.debug("Exception: " + e.getLocalizedMessage());
          }
        }
      } finally {
        lock.unlock();
      }

      allEvents.addAll(events);
    }

    /** shared calendar */
    qm  = calendarShareNode.getSession().getWorkspace().getQueryManager();

    if (calendarShareNode.hasNode(username)) {
      PropertyIterator propertyIterator = calendarShareNode.getNode(username).getReferences();
      while (propertyIterator.hasNext()) {
        try {

          Node calendar = propertyIterator.nextProperty().getParent();
          StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
              .append(" WHERE jcr:path LIKE '").append(calendar.getPath()).append("/%'")
              .append(" AND NOT jcr:path LIKE '").append(calendar.getPath()).append("/%/%'")
              .append(" AND NOT (exo:toDateTime < TIMESTAMP '" + ISO8601.format(eventQuery.getFromDate()) + "'")
              .append(" OR exo:fromDateTime > TIMESTAMP '" + ISO8601.format(eventQuery.getToDate()) + "')")
              .append(" AND NOT (jcr:mixinTypes='exo:repeatCalendarEvent' AND NOT exo:repeat='norepeat' AND exo:recurrenceId='')");

          /**
           StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(path + "/" + calendarId)
           .append("/element(*,exo:calendarEvent)")
           .append(" [ not(@exo:toDateTime < xs:dateTime('" + ISO8601.format(eventQuery.getFromDate()) + "')")
           .append(" or @exo:fromDateTime > xs:dateTime('" + ISO8601.format(eventQuery.getToDate()) + "'))")
           .append(" and not(@jcr:mixinTypes='exo:repeatCalendarEvent' and @exo:repeat!='norepeat' and @exo:recurrenceId='')]")
           .append(" /(@exo:calendarId, @exo:toDateTime, @exo:fromDateTime)");
           **/

          /** event type */
          String eventType = eventQuery.getEventType();
          if (eventType != null && eventType.length() > 0) {
            queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
          }

          /** event category */

          if (!eventQuery.isSearchInAllCategories()) {
            String[] categoryIds = eventQuery.getCategoryId();
            queryEventsStatementSQL.append(" AND (");
            for (int i = 0; i < categoryIds.length; i++) {
              if (i == 0)
                queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
              else
                queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
            }
            queryEventsStatementSQL.append(")");
          }

          query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);

          try {
            NodeIterator iterator = query.execute().getNodes();
            CalendarEvent event;
            while (iterator.hasNext()) {
              event = getEvent(iterator.nextNode());
              event.setCalType(String.valueOf(Calendar.TYPE_SHARED));
              allEvents.add(event);
            }
          } catch (RepositoryException e) {
            log.debug("RepositoryException: " + e.getLocalizedMessage());
          } catch (Exception e) {
            log.debug("Exception: " + e.getLocalizedMessage());
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Fail to update map of shared event", e);
          }
        }
      }
    }

    return allEvents;
  }


  /**
   * Another version of getOriginalRecurrenceEvents using Thread
   *
   * @param username
   * @param from
   * @param to
   * @param publicCalendarIds
   * @return
   * @throws Exception
   */
  public List<CalendarEvent> getHighLightOriginalRecurrenceEvents(String username,
                                                                  java.util.Calendar from,
                                                                  java.util.Calendar to,
                                                                  String[] publicCalendarIds) throws Exception {
    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();
    CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));

    /** get from user private calendars, with filter settings  */
    Node calendarHome = getUserCalendarHome(username);
    String[] filterIds = calSetting.getFilterPrivateCalendars();

    CountDownLatch latch = new CountDownLatch(2);
    QueryRecurrentEventsTask privateEventsQueryTask = new QueryRecurrentEventsTask(calendarHome,
                                                                                   String.valueOf(Calendar.TYPE_PRIVATE), from, to, null, filterIds, latch);
    Thread privateEventsQueryThread = new Thread(privateEventsQueryTask);
    privateEventsQueryThread.start();

    /** get from public calendars, with filter settings */
    Node publicCalendarHome = getPublicCalendarHome();
    filterIds = calSetting.getFilterPublicCalendars();
    QueryRecurrentEventsTask publicEventsQueryTask = new QueryRecurrentEventsTask(publicCalendarHome,
                                                                                  String.valueOf(Calendar.TYPE_PUBLIC), from, to, publicCalendarIds, filterIds, latch);
    Thread publicEventsQueryThread = new Thread(publicEventsQueryTask);
    publicEventsQueryThread.start();
    /** get from shared calendars, with filter settings */
    Node sharedCalendarHome = getSharedCalendarHome();
    filterIds = calSetting.getFilterSharedCalendars();
    List<String> filterIdList = null;
    if (filterIds != null && filterIds.length > 0)
      filterIdList = Arrays.asList(filterIds);
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      PropertyIterator iter = userNode.getReferences();
      Node calendar;
      while (iter.hasNext()) {
        calendar = iter.nextProperty().getParent();
        if (filterIdList != null && filterIdList.contains(calendar.getProperty(Utils.EXO_ID).getString()))
          continue;
        recurEvents.addAll(getOriginalRecurrenceEvents1(calendar, String.valueOf(Calendar.TYPE_SHARED),
                                                        from, to, null, null));
      }
    }

    latch.await();
    recurEvents.addAll(privateEventsQueryTask.calendarEvents);
    recurEvents.addAll(publicEventsQueryTask.calendarEvents);
    return recurEvents;
  }


  /**
   * {@inheritDoc}
   */
  public List<CalendarEvent> getEvents(String username,
                                       EventQuery eventQuery,
                                       String[] publicCalendarIds) throws Exception {
    List<CalendarEvent> events = new ArrayList<CalendarEvent>();
    List<String> filterList = new ArrayList<String>();
    CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));
    filterList.addAll(Arrays.asList(calSetting.getFilterPrivateCalendars()));
    filterList.addAll(Arrays.asList(calSetting.getFilterPublicCalendars()));
    filterList.addAll(Arrays.asList(calSetting.getFilterSharedCalendars()));
    eventQuery.setFilterCalendarIds(filterList.toArray(new String[] {}));
    events.addAll(getUserEvents(username, eventQuery));

    try {
      events.addAll(getSharedEvents(username, eventQuery));

      if (publicCalendarIds != null && publicCalendarIds.length > 0) {
        eventQuery.setCalendarId(publicCalendarIds);
        events.addAll(getPublicEvents(eventQuery));
      }

      // add recurrence events
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug("Fail to get events", e);
      }
    }
    return events;
  }

  /**
   * Another version of getOriginalRecurrenceEvents
   */
  public List<CalendarEvent> getHighLightOriginalRecurrenceEventsSQL(String username,java.util.Calendar from,
                                                                     java.util.Calendar to, EventQuery eventQuery, String[] privateCalendars,
                                                                     String[] publicCalendars, List<String> emptyCalendars) throws Exception {
    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();

    /** get from user private calendars */
    recurEvents.addAll(getOriginalRecurrenceEventsSQL(getUserCalendarHome(username), String.valueOf(Calendar.TYPE_PRIVATE),
        from, to, eventQuery, privateCalendars, emptyCalendars));

    Node publicHome = getPublicCalendarHome();
    QueryManager qm = publicHome.getSession().getWorkspace().getQueryManager();

    for (String calendarId : publicCalendars) {
      if (emptyCalendars!= null && emptyCalendars.contains(calendarId)) continue;

      String calendarPath = publicHome.getPath() + "/" + calendarId;

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%/%'")
          .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
          .append(" AND NOT exo:repeat='norepeat'")
          .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
          .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      Lock lock = getLock("GroupCalendarRecurrentEvent", queryEventsStatementSQL.toString());
      lock.lock();
      List<CalendarEvent> events;

      try {
        events = groupCalendarRecurrentEventCache.get(queryEventsStatementSQL.toString());
        if (events == null) {
          events = new ArrayList<CalendarEvent>();
          Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);

          try {
            NodeIterator it = query.execute().getNodes();
            while (it.hasNext()) {
              Node eventNode = it.nextNode();
              CalendarEvent event = getEvent(eventNode);
              event.setCalType(String.valueOf(Calendar.TYPE_PUBLIC));
              events.add(event);
            }

            groupCalendarRecurrentEventCache.put(queryEventsStatementSQL.toString(), events);

          } catch (RepositoryException e) {
            log.debug("RepositoryException: " + e.getLocalizedMessage());
          } catch (Exception e) {
            log.debug("Exception: " + e.getLocalizedMessage());
          }
        }
      } finally {
        lock.unlock();
      }

      recurEvents.addAll(events);
    }

    Node sharedCalendarHome = getSharedCalendarHome();
    qm = sharedCalendarHome.getSession().getWorkspace().getQueryManager();

    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      PropertyIterator propertyIterator = userNode.getReferences();
      Node calendarNode;
      while (propertyIterator.hasNext()) {
        calendarNode = propertyIterator.nextProperty().getParent();

        StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
            .append(" WHERE jcr:path LIKE '").append(calendarNode.getPath()).append("/%'")
            .append(" AND NOT jcr:path LIKE '").append(calendarNode.getPath()).append("/%/%'")
            .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
            .append(" AND NOT exo:repeat='norepeat'")
            .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
            .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

        /** event type */
        String eventType = eventQuery.getEventType();
        if (eventType != null && eventType.length() > 0) {
          queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
        }

        /** event category */
        if (!eventQuery.isSearchInAllCategories()) {
          String[] categoryIds = eventQuery.getCategoryId();
          queryEventsStatementSQL.append(" AND (");
          for (int i = 0; i < categoryIds.length; i++) {
            if (i == 0)
              queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
            else
              queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
          }
          queryEventsStatementSQL.append(")");
        }

        Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
        QueryResult result = query.execute();

        NodeIterator it = result.getNodes();
        while (it.hasNext()) {
          Node eventNode = it.nextNode();
          CalendarEvent event = getEvent(eventNode);
          event.setCalType(String.valueOf(Calendar.TYPE_SHARED));
          recurEvents.add(event);
        }

      }
    }

    return recurEvents;
  }


  public List<CalendarEvent> searchHighLightOriginalRecurrenceEventsSQL(String username,java.util.Calendar from,
                                                                        java.util.Calendar to, EventQuery eventQuery,
                                                                        String[] privateCalendars, String[] publicCalendars) throws Exception {
    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();
    StringBuffer emptyCalendars = null;

    /** query private calendars */
    Node calendarHome = getUserCalendarHome(username);
    QueryManager qm = calendarHome.getSession().getWorkspace().getQueryManager();

    for (String calendarId : privateCalendars) {
      String calendarPath = calendarHome.getPath() + "/" + calendarId;

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%/%'")
          .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
          .append(" AND NOT exo:repeat='norepeat'")
          .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
          .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

      /**
      StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(calendarPath)
          .append("/element(*,exo:repeatCalendarEvent) [@exo:repeat!='norepeat' and @exo:recurrenceId=''")
          .append(" and (not(@exo:repeatUntil) or @exo:repeatUntil >= xs:dateTime('" + ISO8601.format(from) + "'))")
          .append(" and (not(@exo:repeatFinishDate) or @exo:repeatFinishDate >= xs:dateTime('" + ISO8601.format(from) + "'))")
          .append("]");
      **/

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
      QueryResult result = query.execute();

      NodeIterator it = result.getNodes();
      if (!it.hasNext()) {

        if (emptyCalendars == null) {
          emptyCalendars = new StringBuffer();
          emptyCalendars.append(calendarId);
        }
        else emptyCalendars.append(";" + calendarId);
      }
      else {
        while (it.hasNext()) {
          Node eventNode = it.nextNode();
          CalendarEvent event = getEvent(eventNode);
          event.setCalType(String.valueOf(Calendar.TYPE_PRIVATE));
          recurEvents.add(event);
        }
      }
    }

    /** query public calendar evens */
    Node publicHome = getPublicCalendarHome();
    qm = publicHome.getSession().getWorkspace().getQueryManager();

    for (String calendarId : publicCalendars) {
      String calendarPath = publicHome.getPath() + "/" + calendarId;

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%/%'")
          .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
          .append(" AND NOT exo:repeat='norepeat'")
          .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
          .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      Lock lock = getLock("GroupCalendarRecurrentEvent", queryEventsStatementSQL.toString());
      lock.lock();
      List<CalendarEvent> events;

      try {
        events = groupCalendarRecurrentEventCache.get(queryEventsStatementSQL.toString());
        if (events == null) {
          Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
          events = new ArrayList<CalendarEvent>();

          try {
            NodeIterator it = query.execute().getNodes();
            if (!it.hasNext()) {
              if (emptyCalendars == null) {
                emptyCalendars = new StringBuffer();
                emptyCalendars.append(calendarId);
              }
              else emptyCalendars.append(";" + calendarId);
            }
            else {

              while (it.hasNext()) {
                Node eventNode = it.nextNode();
                CalendarEvent event = getEvent(eventNode);
                event.setCalType(String.valueOf(Calendar.TYPE_PUBLIC));
                events.add(event);
              }

              groupCalendarRecurrentEventCache.put(queryEventsStatementSQL.toString(), events);
            }

          } catch (RepositoryException e) {
            log.debug("RepositoryException: " + e.getLocalizedMessage());
          } catch (Exception e) {
            log.debug("Exception: " + e.getLocalizedMessage());
          }
        }
      } finally {
        lock.unlock();
      }

      recurEvents.addAll(events);
    }

    /** query shared calendar event */
    Node sharedCalendarHome = getSharedCalendarHome();
    qm = sharedCalendarHome.getSession().getWorkspace().getQueryManager();

    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      PropertyIterator propertyIterator = userNode.getReferences();
      Node calendarNode;
      while (propertyIterator.hasNext()) {
        calendarNode = propertyIterator.nextProperty().getParent();

        StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
            .append(" WHERE jcr:path LIKE '").append(calendarNode.getPath()).append("/%'")
            .append(" AND NOT jcr:path LIKE '").append(calendarNode.getPath()).append("/%/%'")
            .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
            .append(" AND NOT exo:repeat='norepeat'")
            .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
            .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

        /** event type */
        String eventType = eventQuery.getEventType();
        if (eventType != null && eventType.length() > 0) {
          queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
        }

        /** event category */
        if (!eventQuery.isSearchInAllCategories()) {
          String[] categoryIds = eventQuery.getCategoryId();
          queryEventsStatementSQL.append(" AND (");
          for (int i = 0; i < categoryIds.length; i++) {
            if (i == 0)
              queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
            else
              queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
          }
          queryEventsStatementSQL.append(")");
        }

        Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
        QueryResult result = query.execute();

        NodeIterator it = result.getNodes();
        while (it.hasNext()) {
          Node eventNode = it.nextNode();
          CalendarEvent event = getEvent(eventNode);
          event.setCalType(String.valueOf(Calendar.TYPE_SHARED));
          recurEvents.add(event);
        }


      }
    }

    if (emptyCalendars != null) {
      CalendarEvent emptyCalEvent = new CalendarEvent();
      emptyCalEvent.setCalendarId("-1");
      emptyCalEvent.setSummary(emptyCalendars.toString());
      recurEvents.add(0, emptyCalEvent);
    }

    return recurEvents;
  }

  /**
   * Get all active 'original' recurrence event <br>
   * The result list includes all 'original' recurrence event in all three
   * types: private, public and shared
   * 
   * @param username
   * @return
   * @throws Exception
   */
  public List<CalendarEvent> getOriginalRecurrenceEvents(String username,
                                                         java.util.Calendar from,
                                                         java.util.Calendar to,
                                                         String[] publicCalendarIds) throws Exception {
    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();
    CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));

    /** get from user private calendars, with filter settings  */
    Node calendarHome = getUserCalendarHome(username);
    String[] filterIds = calSetting.getFilterPrivateCalendars();
    recurEvents.addAll(getOriginalRecurrenceEvents1(calendarHome,
                                                    String.valueOf(Calendar.TYPE_PRIVATE), from, to, null, filterIds));

    // get from share calendars, with filter settings
    Node sharedCalendarHome = getSharedCalendarHome();
    filterIds = calSetting.getFilterSharedCalendars();
    List<String> filterIdList = null;
    if (filterIds != null && filterIds.length > 0)
      filterIdList = Arrays.asList(filterIds);
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      PropertyIterator iter = userNode.getReferences();
      Node calendar;
      while (iter.hasNext()) {
        calendar = iter.nextProperty().getParent();
        if (filterIdList != null
            && filterIdList.contains(calendar.getProperty(Utils.EXO_ID).getString()))
          continue;
        recurEvents.addAll(getOriginalRecurrenceEvents1(calendar,
                                                        String.valueOf(Calendar.TYPE_SHARED),
                                                        from,
                                                        to,
                                                        null,
                                                        null));
      }
    }

    /** get from public calendars, with filter settings */
    Node publicCalendarHome = getPublicCalendarHome();
    filterIds = calSetting.getFilterPublicCalendars();
    recurEvents.addAll(getOriginalRecurrenceEvents1(publicCalendarHome,
                                                    String.valueOf(Calendar.TYPE_PUBLIC),
                                                    from,
                                                    to,
                                                    publicCalendarIds,
                                                    filterIds));
    return recurEvents;
  }


  /**
   * A faster version getOriginalRecurrenceEvents with improved JCR queries
   *
   * @param calendar
   * @param calType
   * @param from
   * @param to
   * @param calendarIds
   * @param filterCalendarIds
   * @return
   * @throws Exception
   */
  public List<CalendarEvent> getOriginalRecurrenceEvents1(Node calendar, String calType, java.util.Calendar from,
                                                          java.util.Calendar to, String[] calendarIds, String[] filterCalendarIds)
                                                              throws Exception {
    if (calendar == null) return null;
    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();
    StringBuilder queryString = new StringBuilder("/jcr:root").append(XPathUtils.escapeIllegalXPathName(calendar.getPath()))
        .append("//element(*,exo:repeatCalendarEvent) [@exo:repeat!='norepeat' and @exo:recurrenceId=''")
        .append(" and (not(@exo:repeatUntil) or @exo:repeatUntil >= xs:dateTime('" + ISO8601.format(from) + "'))")
        .append(" and (not(@exo:repeatFinishDate) or @exo:repeatFinishDate >= xs:dateTime('" + ISO8601.format(from) + "'))");

    /** search within calendars */
    if (calendarIds != null && calendarIds.length > 0) {
      queryString.append(" and (");
      for (int i = 0; i < calendarIds.length; i++) {
        if (i == 0)
          queryString.append("@exo:calendarId = '").append(calendarIds[i]).append("'");
        else
          queryString.append(" or @exo:calendarId = '").append(calendarIds[i]).append("'");
      }
      queryString.append(")");
    }

    /** exclude calendars */
    if (filterCalendarIds != null && filterCalendarIds.length > 0) {
      queryString.append(" and (");
      for (int i = 0; i < filterCalendarIds.length; i++) {
        if (i == 0)
          queryString.append("@exo:calendarId != '").append(filterCalendarIds[i]).append("'");
        else
          queryString.append(" and @exo:calendarId != '").append(filterCalendarIds[i]).append("'");
      }
      queryString.append(")");
    }

    /**  Add event query to this
    String eventType = eventQuery.getEventType();
    if (eventType != null && eventType.length() > 0) {
      queryStatement.append(" and (@exo:eventType='" + eventType + "')");
    }

    String categoryIds = eventQuery.getCategoryId();
    if (categoryIds != null && categoryIds.length > 0) {
      queryStatement.append(" and (");
      for (int i = 0; i < categoryIds.length; i++) {
        if (i == 0)
          queryStatement.append("@exo:eventCategoryId='").append(categoryIds[i]).append("'");
        else
          queryStatement.append(" or @exo:eventCategoryId='").append(categoryIds[i]).append("'");
      }
      queryStatement.append(")");
    }
     **/

    queryString.append("]");
    QueryManager qm = calendar.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();

    NodeIterator it = result.getNodes();
    while (it.hasNext()) {
      Node eventNode = it.nextNode();
      CalendarEvent event = getEvent(eventNode);
      event.setCalType(calType);
      recurEvents.add(event);
    }
    return recurEvents;
  }

  /**
   * Another version for getOriginalRecurrenceEvents1 using SQL with different queries
   *
   * @param calendar
   * @param calType
   * @param from
   * @param to
   * @param calendarIds
   *
   * @return
   * @throws Exception
   */
  public List<CalendarEvent> getOriginalRecurrenceEventsSQL(Node calendar, String calType, java.util.Calendar from,
                                                            java.util.Calendar to, EventQuery eventQuery,
                                                            String[] calendarIds, List<String> emptyCalendars) throws Exception {
    if (calendar == null) return null;

    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();

    for (String calendarId : calendarIds) {
      /** skip the calendar if it's empty */
      if (emptyCalendars!= null && emptyCalendars.contains(calendarId)) continue;

      String calendarPath = calendar.getPath() + "/" + calendarId;

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%/%'")
          .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
          .append(" AND NOT exo:repeat='norepeat'")
          .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
          .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

      StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(calendarPath)
          .append("/element(*,exo:repeatCalendarEvent) [@exo:repeat!='norepeat' and @exo:recurrenceId=''")
          .append(" and (not(@exo:repeatUntil) or @exo:repeatUntil >= xs:dateTime('" + ISO8601.format(from) + "'))")
          .append(" and (not(@exo:repeatFinishDate) or @exo:repeatFinishDate >= xs:dateTime('" + ISO8601.format(from) + "'))")
          .append("]");

      /** event type */
      String eventType = eventQuery.getEventType();
      if (eventType != null && eventType.length() > 0) {
        queryEventsStatementSQL.append(" AND (exo:eventType='" + eventType + "')");
      }

      /** event category */
      if (!eventQuery.isSearchInAllCategories()) {
        String[] categoryIds = eventQuery.getCategoryId();
        queryEventsStatementSQL.append(" AND (");
        for (int i = 0; i < categoryIds.length; i++) {
          if (i == 0)
            queryEventsStatementSQL.append("exo:eventCategoryId='").append(categoryIds[i]).append("'");
          else
            queryEventsStatementSQL.append(" OR exo:eventCategoryId='").append(categoryIds[i]).append("'");
        }
        queryEventsStatementSQL.append(")");
      }

      QueryManager qm = calendar.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);

      //Query query = qm.createQuery(queryEventsStatementXPath.toString(), Query.XPATH);
      QueryResult result = query.execute();

      NodeIterator it = result.getNodes();
      while (it.hasNext()) {
        Node eventNode = it.nextNode();
        CalendarEvent event = getEvent(eventNode);
        event.setCalType(calType);
        recurEvents.add(event);
      }
    }

    return recurEvents;
  }

  //This method is not used anywhere
  public List<CalendarEvent> searchOriginalRecurrenceEventsSQL(Node calendar, String calType, java.util.Calendar from,
                                                               java.util.Calendar to, String[] calendarIds, List<String> emptyCalendars) throws Exception {
    if (calendar == null) return null;

    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();

    for (String calendarId : calendarIds) {
      String calendarPath = calendar.getPath() + "/" + calendarId;

      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
          .append(" WHERE jcr:path LIKE '").append(calendarPath).append("/%'")
          .append(" AND NOT jcr:path LIKE '").append(calendarPath).append("/%/%'")
          .append(" AND jcr:mixinTypes='exo:repeatCalendarEvent'")
          .append(" AND NOT exo:repeat='norepeat'")
          .append(" AND (exo:repeatUntil IS NULL OR exo:repeatUntil >=  TIMESTAMP '" + ISO8601.format(from) +  "' )")
          .append(" AND (exo:repeatFinishDate IS NULL OR exo:repeatFinishDate >=  TIMESTAMP '" + ISO8601.format(from) +  "' )");

      /**
      StringBuilder queryEventsStatementXPath = new StringBuilder("/jcr:root").append(calendarPath)
          .append("/element(*,exo:repeatCalendarEvent) [@exo:repeat!='norepeat' and @exo:recurrenceId=''")
          .append(" and (not(@exo:repeatUntil) or @exo:repeatUntil >= xs:dateTime('" + ISO8601.format(from) + "'))")
          .append(" and (not(@exo:repeatFinishDate) or @exo:repeatFinishDate >= xs:dateTime('" + ISO8601.format(from) + "'))")
          .append("]");
      **/

      QueryManager qm = calendar.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryEventsStatementSQL.toString(), Query.SQL);
      QueryResult result = query.execute();

      NodeIterator it = result.getNodes();
      if (!it.hasNext()) emptyCalendars.add(calendarId);
      else {
        while (it.hasNext()) {
          Node eventNode = it.nextNode();
          CalendarEvent event = getEvent(eventNode);
          event.setCalType(calType);
          recurEvents.add(event);
        }
      }
    }

    return recurEvents;
  }

  public List<CalendarEvent> getOriginalRecurrenceEvents(Node calendar,
                                                         String calType,
                                                         java.util.Calendar from,
                                                         java.util.Calendar to,
                                                         String[] calendarIds,
                                                         String[] filterCalendarIds) throws Exception {
    if (calendar == null)
      return null;
    List<CalendarEvent> recurEvents = new ArrayList<CalendarEvent>();
    StringBuilder queryString = new StringBuilder("/jcr:root").append(XPathUtils.escapeIllegalXPathName(calendar.getPath()))
        .append("//element(*,exo:repeatCalendarEvent)[@exo:repeat!='").append(CalendarEvent.RP_NOREPEAT)
        .append("' and @exo:recurrenceId=''");
    if (from != null) {
      queryString.append(" and (not(@exo:repeatUntil) or @exo:repeatUntil >= xs:dateTime('"
          + ISO8601.format(from)).append("'))")
          .append(" and (not(@exo:repeatFinishDate) or @exo:repeatFinishDate >= xs:dateTime('"
              + ISO8601.format(from)).append("'))");
    } else {
      queryString.append(" and (not(@exo:repeatUntil))")
      .append(" and (not(@exo:repeatFinishDate))");
    }
    if (calendarIds != null && calendarIds.length > 0) {
      queryString.append(" and (");
      for (int i = 0; i < calendarIds.length; i++) {
        if (i == 0)
          queryString.append("@exo:calendarId = '").append(calendarIds[i]).append("'");
        else
          queryString.append(" or @exo:calendarId = '").append(calendarIds[i]).append("'");
      }
      queryString.append(")");
    }

    if (filterCalendarIds != null && filterCalendarIds.length > 0) {
      queryString.append(" and (");
      for (int i = 0; i < filterCalendarIds.length; i++) {
        if (i == 0)
          queryString.append("@exo:calendarId != '").append(filterCalendarIds[i]).append("'");
        else
          queryString.append(" and @exo:calendarId != '").append(filterCalendarIds[i]).append("'");
      }
      queryString.append(")");
    }
    queryString.append("]");

    QueryManager qm = calendar.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator it = result.getNodes();
    while (it.hasNext()) {
      Node eventNode = it.nextNode();
      CalendarEvent event = getEvent(eventNode);
      event.setCalType(calType);
      recurEvents.add(event);
    }
    return recurEvents;
  }

  /**
   * Get all exception occurrence from original recurrence event
   * 
   * @param username
   * @param recurEvent
   * @return
   * @throws Exception
   */
  public List<CalendarEvent> getExceptionEvents(String username, CalendarEvent recurEvent) throws Exception {

    if (recurEvent == null || recurEvent.getRepeatType().equals(CalendarEvent.RP_NOREPEAT))
      return null;
    try {
      Node calendarHome = null;
      int calType = Integer.parseInt(recurEvent.getCalType());
      Node eventNode = null;
      if (calType == Calendar.TYPE_PRIVATE) {
        calendarHome = getUserCalendarHome(username);
        eventNode = calendarHome.getNode(recurEvent.getCalendarId()).getNode(recurEvent.getId());
      }
      if (calType == Calendar.TYPE_SHARED) {
        calendarHome = getSharedCalendarHome();
        if (calendarHome.hasNode(username)) {
          PropertyIterator iter = calendarHome.getNode(username).getReferences();
          while (iter.hasNext()) {
            Node calendar = iter.nextProperty().getParent();
            if (!recurEvent.getCalendarId().equals(calendar.getProperty(Utils.EXO_ID)))
              continue;
            if (calendar.hasNode(recurEvent.getId())) {
              eventNode = calendar.getNode(recurEvent.getId());
            }
          }
        }
      }
      if (calType == Calendar.TYPE_PUBLIC) {
        calendarHome = getPublicCalendarHome();
        eventNode = calendarHome.getNode(recurEvent.getCalendarId()).getNode(recurEvent.getId());
      }

      if (eventNode == null || !eventNode.isNodeType(Utils.MIX_REFERENCEABLE))
        return null;
      PropertyIterator propIter = eventNode.getReferences();
      if (propIter == null)
        return null;
      List<CalendarEvent> exceptions = new ArrayList<CalendarEvent>();
      while (propIter.hasNext()) {
        Property prop = propIter.nextProperty();
        Node exceptionNode = prop.getParent();
        CalendarEvent exception = getEvent(exceptionNode);
        exception.setCalType(String.valueOf(calType));
        exceptions.add(exception);
      }
      return exceptions;
    } catch (Exception e) {
      log.error("Exception occurred when finding all exception occurrences", e);
      return null;
    }
  }

  /**
   * Query all occurrences of recurrence event between from and to date. <br>
   * The result contains only 'virtual' occurrence events, no exception
   * occurrences
   * 
   * @param recurEvent the original recurrence event
   * @param from
   * @param to
   * @param timezone
   * @return
   * @throws Exception
   */
  public Map<String, CalendarEvent> getOccurrenceEvents(CalendarEvent recurEvent,
                                                        java.util.Calendar from,
                                                        java.util.Calendar to,
                                                        String timezone) throws Exception {
    if (Utils.isEmpty(recurEvent.getRepeatType()))
      return null;
    if (from.after(to) || !recurEvent.getFromDateTime().before(to.getTime())) {
      return null;
    }

    // check if this recurEvent is expired
    if (recurEvent.getRepeatUntilDate() != null
        && recurEvent.getRepeatUntilDate().before(from.getTime())) {
      return null;
    }

    TimeZone userTimeZone = DateUtils.getTimeZone(timezone);
    SimpleDateFormat format = new SimpleDateFormat(Utils.DATE_FORMAT_RECUR_ID);
    format.setTimeZone(userTimeZone);

    Map<String, CalendarEvent> occurrences = new HashMap<String, CalendarEvent>();

    int diffMinutes = (int) ((recurEvent.getToDateTime().getTime() - recurEvent.getFromDateTime()
        .getTime()) / (60 * 1000));

    Recur recur = Utils.getICalendarRecur(recurEvent);
    if (recur == null)
      return null;

    DateTime ical4jEventFrom = new DateTime(recurEvent.getFromDateTime());//the date time of the first occurrence of the series
    net.fortuna.ical4j.model.TimeZone tz = Utils.getICalTimeZone(DateUtils.getTimeZone(timezone));
    ical4jEventFrom.setTimeZone(tz);

    Utils.adaptRepeatRule(recur, recurEvent.getFromDateTime(), CalendarService.PERSISTED_TIMEZONE, userTimeZone);

    java.util.Calendar occurenceFrom = java.util.Calendar.getInstance();
    occurenceFrom.setTime(from.getTime());

    // because occurrence event can begin before the 'from' but end after
    // 'from', so it still intersects with [from, to] window
    // thus, we decrease the 'from' value (by duration of event) to get such
    // occurrences
    occurenceFrom.add(java.util.Calendar.MINUTE, -(diffMinutes - 1));
    DateTime ical4jFrom = new DateTime(occurenceFrom.getTime());
    DateTime ical4jTo = new DateTime(to.getTime());
    Period period = new Period(ical4jFrom, ical4jTo);
    period.setTimeZone(tz);
    // get list of occurrences in a period
    DateList list = recur.getDates(ical4jEventFrom,
                                   period,
                                   net.fortuna.ical4j.model.parameter.Value.DATE_TIME);

    for (Object dt : list) {
      DateTime ical4jStart = (DateTime) dt;
      ical4jStart.setTimeZone(tz);

      // make occurrence
      CalendarEvent occurrence = new CalendarEvent(recurEvent);

      java.util.Calendar startTime = java.util.Calendar.getInstance(TimeZone.getDefault());
      java.util.Calendar endTime = java.util.Calendar.getInstance(TimeZone.getDefault());
      startTime.setTimeInMillis(ical4jStart.getTime());

      occurrence.setFromDateTime(startTime.getTime());

      endTime.setTime(startTime.getTime());
      endTime.add(java.util.Calendar.MINUTE, diffMinutes);

      occurrence.setToDateTime(endTime.getTime());

      // if user time zone uses DST, need to adapt the time occurrence 
      if( userTimeZone.useDaylightTime()) {
        adaptTimeToDST(occurrence, recurEvent, userTimeZone);    
      }

      String recurId = format.format(occurrence.getFromDateTime());

      // if this occurrence was listed in the exclude list, skip
      boolean isException = false ;
      if(recurEvent.getExceptionIds() != null)
        for(String s : recurEvent.getExcludeId()){
          if(s.split("T")[0].equals(recurId.split("T")[0])) {
            isException = true;
            break ;
          }
        }
      if(isException)continue;

      occurrence.setRecurrenceId(recurId);
      occurrences.put(recurId, occurrence);
    }
    return occurrences;
  }

  // here we need to edit the occurrence to have correct time 
  // following the DST condition of created date and occur date
  private void adaptTimeToDST(CalendarEvent occurrence, CalendarEvent recurEvent, TimeZone userTimeZone) {
    java.util.Calendar from = java.util.Calendar.getInstance(userTimeZone);
    java.util.Calendar to = java.util.Calendar.getInstance(userTimeZone);
    from.setTime(occurrence.getFromDateTime());
    to.setTime(occurrence.getToDateTime());

    int dstUser = userTimeZone.getDSTSavings() / 3600000;

    Boolean originalUseDst = userTimeZone.inDaylightTime(recurEvent.getFromDateTime()); 
    Boolean occurenceUseDst = userTimeZone.inDaylightTime(occurrence.getFromDateTime());

    // if original created date uses DST but occur date does not use DST
    if(originalUseDst && !occurenceUseDst) {
      from.add(java.util.Calendar.HOUR, dstUser);
      to.add(java.util.Calendar.HOUR, dstUser);
    }

    // if occur date uses DST but original created date does not use DST
    if(!originalUseDst && occurenceUseDst) {
      from.add(java.util.Calendar.HOUR, -dstUser);
      to.add(java.util.Calendar.HOUR, -dstUser);
    }

    occurrence.setFromDateTime(from.getTime());
    occurrence.setToDateTime(to.getTime());
  }
  /**
   * Calculate the end-date of recurrence event
   * 
   * @param originalEvent
   * @return
   * @throws Exception
   */
  public Date calculateRecurrenceFinishDate(CalendarEvent originalEvent) throws Exception {
    try {
      if (originalEvent.getRepeatUntilDate() != null) {
        return originalEvent.getRepeatUntilDate();
      }

      // in case of repeat forever event
      if (originalEvent.getRepeatCount() <= 0) {
        return null;
      }

      DateTime ical4jEventFrom = new DateTime(originalEvent.getFromDateTime());
      VEvent vevent = new VEvent(ical4jEventFrom, Utils.EMPTY_STR);

      Recur recur = Utils.getICalendarRecur(originalEvent);

      vevent.getProperties().add(new RRule(recur));
      java.util.Calendar calendar = new GregorianCalendar(2011, 7, 1);
      calendar.setTimeZone(DateUtils.getTimeZone("GMT"));
      calendar.set(java.util.Calendar.YEAR, calendar.getMinimum(java.util.Calendar.YEAR));
      DateTime ical4jFrom = new DateTime(calendar.getTime());
      calendar.set(java.util.Calendar.YEAR, calendar.getMaximum(java.util.Calendar.YEAR));
      DateTime ical4jTo = new DateTime(calendar.getTime());
      Period period = new Period(ical4jFrom, ical4jTo);
      PeriodList list = vevent.calculateRecurrenceSet(period);
      if (list == null || list.size() == 0)
        return null;
      Period last = (Period) list.last();
      calendar.setTimeInMillis(last.getStart().getTime());
      calendar.add(java.util.Calendar.DATE, 1);
      calendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
      calendar.set(java.util.Calendar.MINUTE, 0);
      return calendar.getTime();
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug("Exception occurred when calculating finish date of recurrence event", e);
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Map<String, String> checkFreeBusy(EventQuery eventQuery) throws Exception {
    Date fromDate = eventQuery.getFromDate().getTime();
    Date toDate = eventQuery.getToDate().getTime();
    Map<String, String> participantMap = new HashMap<String, String>();
    participantMap.putAll(checkFreeBusy(eventQuery, fromDate));
    if (!Utils.isSameDate(fromDate, toDate)) {
      Map<String, String> remainingInfo = checkFreeBusy(eventQuery, toDate);
      if (remainingInfo.size() > 0) {
        StringBuilder sb = new StringBuilder();
        for (String par : remainingInfo.keySet()) {
          sb.append(remainingInfo.get(par));
          if (participantMap.containsKey(par))
            sb.append(Utils.COMMA).append(participantMap.get(par));
          participantMap.put(par, sb.toString());
          sb.setLength(0);
        }
      }
    }
    return participantMap;
  }

  public Map<String, String> checkFreeBusy(EventQuery eventQuery, Date date) throws Exception {
    Node eventFolder = getEventFolder(date);
    Map<String, String> participantMap = new HashMap<String, String>();
    String calendarPath = eventQuery.getCalendarPath();
    String[] orderBy = eventQuery.getOrderBy();
    eventQuery.setCalendarPath(eventFolder.getPath());
    eventQuery.setOrderBy(new String[] { Utils.EXO_FROM_DATE_TIME });
    QueryManager qm = getSession(createSystemProvider()).getWorkspace().getQueryManager();
    String[] pars = eventQuery.getParticipants();
    Query query;
    Node event;
    String from;
    String to;
    for (String par : pars) {
      String[] participants = eventQuery.getParticipants();
      eventQuery.setParticipants(new String[] { par });
      query = qm.createQuery(eventQuery.getQueryStatement(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator it = result.getNodes();
      StringBuilder timeValues = new StringBuilder();
      while (it.hasNext()) {
        event = it.nextNode();
        if (event.hasProperty(Utils.EXO_EVENT_STATE)
            && !CalendarEvent.ST_AVAILABLE.equals(event.getProperty(Utils.EXO_EVENT_STATE)
                                                  .getValue()
                                                  .getString())) {
          java.util.Calendar fromCal = event.getProperty(Utils.EXO_FROM_DATE_TIME).getDate();
          java.util.Calendar toCal = event.getProperty(Utils.EXO_TO_DATE_TIME).getDate();
          if (fromCal.getTimeInMillis() < eventQuery.getFromDate().getTimeInMillis())
            from = String.valueOf(eventQuery.getFromDate().getTimeInMillis());
          else
            from = String.valueOf(fromCal.getTimeInMillis());
          if (toCal.getTimeInMillis() > eventQuery.getToDate().getTimeInMillis()) {
            GregorianCalendar cal = new GregorianCalendar();
            cal.setTimeInMillis(eventQuery.getToDate().getTimeInMillis() - 1000);
            to = String.valueOf(cal.getTimeInMillis());
          } else
            to = String.valueOf(toCal.getTimeInMillis());

          if (timeValues != null && timeValues.length() > 0)
            timeValues.append(",");
          timeValues.append(from).append(",").append(to);
          participantMap.put(par, timeValues.toString());
        }
      }
      eventQuery.setParticipants(participants);
    }
    eventQuery.setCalendarPath(calendarPath);
    eventQuery.setOrderBy(orderBy);
    return participantMap;
  }

  /**
   * {@inheritDoc}
   */
  public void removeSharedEvent(String username, String calendarId, String eventId) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      PropertyIterator iter = userNode.getReferences();
      Node calendar;
      while (iter.hasNext()) {
        calendar = iter.nextProperty().getParent();
        if (calendar.getProperty(Utils.EXO_ID).getString().equals(calendarId)) {
          if (calendar.hasNode(eventId)) {
            Node event = calendar.getNode(eventId);
            Node eventFolder = getEventFolder(event.getProperty(Utils.EXO_FROM_DATE_TIME)
                                              .getDate()
                                              .getTime());
            syncRemoveEvent(eventFolder, eventId);
            removeReminder(event);
            event.remove();
          }
          calendar.save();
          calendar.refresh(true);
          break;
        }
      }
      userCalendarCache.remove(username);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void moveEvent(String formCalendar,
                        String toCalendar,
                        String fromType,
                        String toType,
                        List<CalendarEvent> calEvents,
                        String username) throws Exception {
    try {
      switch (Integer.parseInt(fromType)) {
      case Calendar.TYPE_PRIVATE:
        if (getUserCalendarHome(username).hasNode(formCalendar)) {
          switch (Integer.parseInt(toType)) {
          case Calendar.TYPE_PRIVATE:
            // move events in side public calendars
            if (getUserCalendarHome(username).hasNode(toCalendar)) {
              for (CalendarEvent calEvent : calEvents) {
                if (!formCalendar.equals(toCalendar)) {
                  removeUserEvent(username, formCalendar, calEvent.getId());
                  calEvent.setCalendarId(toCalendar);
                  saveUserEvent(username,
                                toCalendar,
                                calEvent,
                                getUserCalendarHome(username).getNode(toCalendar)
                                .hasNode(calEvent.getId()));
                } else {
                  saveUserEvent(username, toCalendar, calEvent, false);
                }
              }
            }
            break;
          case Calendar.TYPE_SHARED:
            // move events form public to shared calendar
            if (getSharedCalendarHome().hasNode(username)) {
              for (CalendarEvent calEvent : calEvents) {
                removeUserEvent(username, formCalendar, calEvent.getId());
                calEvent.setCalendarId(toCalendar);
                saveEventToSharedCalendar(username,
                                          toCalendar,
                                          calEvent,
                                          getSharedCalendarHome().getNode(username)
                                          .hasNode(calEvent.getId()));
              }
            }
            break;
          case Calendar.TYPE_PUBLIC:
            // move events form public to public calendar
            if (getPublicCalendarHome().hasNode(toCalendar)) {
              for (CalendarEvent calEvent : calEvents) {
                removeUserEvent(username, formCalendar, calEvent.getId());
                calEvent.setCalendarId(toCalendar);
                savePublicEvent(toCalendar,
                                calEvent,
                                getPublicCalendarHome().getNode(toCalendar)
                                .hasNode(calEvent.getId()));
              }
            }
            break;
          default:
            break;
          }
        }
        break;
      case Calendar.TYPE_SHARED:
        if (getSharedCalendarHome().hasNode(username)) {
          switch (Integer.parseInt(toType)) {
          case Calendar.TYPE_PRIVATE:
            // move events form share to public calendar
            if (getUserCalendarHome(username).hasNode(toCalendar)) {
              for (CalendarEvent calEvent : calEvents) {
                removeSharedEvent(username, formCalendar, calEvent.getId());
                calEvent.setCalendarId(toCalendar);
                saveUserEvent(username,
                              toCalendar,
                              calEvent,
                              getUserCalendarHome(username).getNode(toCalendar)
                              .hasNode(calEvent.getId()));
              }
            }
            break;
          case Calendar.TYPE_SHARED:
            // move events in side shared calendars
            if (getSharedCalendarHome().hasNode(username)) {
              for (CalendarEvent calEvent : calEvents) {
                if (!formCalendar.equals(toCalendar)) {
                  removeSharedEvent(username, formCalendar, calEvent.getId());
                  calEvent.setCalendarId(toCalendar);
                  saveEventToSharedCalendar(username,
                                            toCalendar,
                                            calEvent,
                                            getSharedCalendarHome().getNode(username)
                                            .hasNode(calEvent.getId()));
                } else {
                  saveEventToSharedCalendar(username, toCalendar, calEvent, false);
                }
              }
            }
            break;
          case Calendar.TYPE_PUBLIC:
            // move events form share to public calendar
            if (getPublicCalendarHome().hasNode(toCalendar)) {
              for (CalendarEvent calEvent : calEvents) {
                removeSharedEvent(username, formCalendar, calEvent.getId());
                calEvent.setCalendarId(toCalendar);
                savePublicEvent(toCalendar,
                                calEvent,
                                getPublicCalendarHome().getNode(toCalendar)
                                .hasNode(calEvent.getId()));
              }
            }
            break;
          default:
            break;
          }
        }
        break;
      case Calendar.TYPE_PUBLIC:
        if (getPublicCalendarHome().hasNode(formCalendar)) {
          switch (Integer.parseInt(toType)) {
          case Calendar.TYPE_PRIVATE:
            // move events from public to public calendar
            if (getUserCalendarHome(username).hasNode(toCalendar)) {
              for (CalendarEvent calEvent : calEvents) {
                removePublicEvent(formCalendar, calEvent.getId());
                calEvent.setCalendarId(toCalendar);
                saveUserEvent(username,
                              toCalendar,
                              calEvent,
                              getUserCalendarHome(username).getNode(toCalendar)
                              .hasNode(calEvent.getId()));
              }
            }
            break;
          case Calendar.TYPE_SHARED:
            // move events from public to shared calendar
            if (getSharedCalendarHome().hasNode(username)) {
              for (CalendarEvent calEvent : calEvents) {
                removePublicEvent(formCalendar, calEvent.getId());
                calEvent.setCalendarId(toCalendar);
                saveEventToSharedCalendar(username, toCalendar, calEvent, true);
              }
            }
            break;
          case Calendar.TYPE_PUBLIC:
            // move events in side public calendars
            if (getPublicCalendarHome().hasNode(toCalendar)) {
              for (CalendarEvent calEvent : calEvents) {
                if (!formCalendar.equals(toCalendar)) {
                  removePublicEvent(formCalendar, calEvent.getId());
                  calEvent.setCalendarId(toCalendar);
                  savePublicEvent(toCalendar,
                                  calEvent,
                                  getPublicCalendarHome().getNode(toCalendar)
                                  .hasNode(calEvent.getId()));
                } else {
                  savePublicEvent(toCalendar, calEvent, false);
                }
              }
            }
            break;
          default:
            break;
          }
        }
        break;
      default:
        break;
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug("Fail to move event", e);
      }
    }
  }

  /**
   * This function is used to update a occurrence event. <br>
   * A occurrence event after update will be an exception occurrence from
   * recurrence series and will be saved as an new event node refers to original
   * node
   * 
   * @param fromCalendar
   * @param toCalendar
   * @param fromCalendarType
   * @param toCalendarType
   * @param events
   * @param username
   * @throws Exception
   */
  public void updateOccurrenceEvent(String fromCalendar,
                                    String toCalendar,
                                    String fromCalendarType,
                                    String toCalendarType,
                                    List<CalendarEvent> events,
                                    String username) throws Exception {
    try {
      int fromType = Integer.parseInt(fromCalendarType);
      int toType = Integer.parseInt(toCalendarType);
      for (CalendarEvent event : events) {
        if (event.getIsExceptionOccurrence() == null || !event.getIsExceptionOccurrence()) {
          // get the original recurrence event to update excludeId property
          CalendarEvent originalEvent = null;
          if (fromType == Calendar.TYPE_PRIVATE)
            originalEvent = getUserEvent(username, fromCalendar, event.getId());
          else if (fromType == Calendar.TYPE_SHARED)
            originalEvent = getSharedEvent(username, fromCalendar, event.getId());
          else if (fromType == Calendar.TYPE_PUBLIC)
            originalEvent = getGroupEvent(fromCalendar, event.getId());

          if (originalEvent != null) {
            List<String> excludeId;
            if (originalEvent.getExcludeId() == null) {
              excludeId = new ArrayList<String>();
            } else {
              excludeId = new ArrayList<String>(Arrays.asList(originalEvent.getExcludeId()));
            }
            excludeId.add(event.getRecurrenceId());
            originalEvent.setExcludeId(excludeId.toArray(new String[0]));

            if (fromType == Calendar.TYPE_PRIVATE)
              saveUserEvent(username, fromCalendar, originalEvent, false);
            else if (fromType == Calendar.TYPE_SHARED)
              saveEventToSharedCalendar(username, fromCalendar, originalEvent, false);
            else if (fromType == Calendar.TYPE_PUBLIC)
              savePublicEvent(fromCalendar, originalEvent, false);
          }
        }

        if (fromCalendar.equals(toCalendar)) {
          if (Utils.isExceptionOccurrence(event)) {
            saveOccurrenceEvent(username, toCalendar, event, false);
          } else {
            saveOccurrenceEvent(username, toCalendar, event, true);
          }
        } else {
          if (Utils.isExceptionOccurrence(event)) {
            if (fromType == Calendar.TYPE_PRIVATE)
              removeUserEvent(username, fromCalendar, event.getId());
            else if (fromType == Calendar.TYPE_SHARED)
              removeSharedEvent(username, fromCalendar, event.getId());
            else if (fromType == Calendar.TYPE_PUBLIC)
              removePublicEvent(fromCalendar, event.getId());
          }
          event.setCalendarId(toCalendar);
          event.setRepeatType(CalendarEvent.RP_NOREPEAT);
          event.setIsExceptionOccurrence(false);

          if (toType == Calendar.TYPE_PRIVATE)
            saveUserEvent(username, toCalendar, event, true);
          else if (toType == Calendar.TYPE_SHARED)
            saveEventToSharedCalendar(username, toCalendar, event, true);
          else if (toType == Calendar.TYPE_PUBLIC)
            savePublicEvent(toCalendar, event, true);
        }
      }
    } catch (Exception e) {
      log.error("Error occurred when updating occurrence event", e);
    }
  }

  /**
   * Update recurrence series
   * 
   * @param fromCalendar
   * @param toCalendar
   * @param fromType
   * @param toType
   * @param occurrence
   * @param username
   * @throws Exception
   */
  public void updateRecurrenceSeries(String fromCalendar,
                                     String toCalendar,
                                     String fromType,
                                     String toType,
                                     CalendarEvent occurrence,
                                     String username) throws Exception {
    try {
      String eventId = occurrence.getId();
      int calType = Integer.parseInt(fromType);
      int toCalType = Integer.parseInt(toType);
      CalendarEvent originalEvent = null;

      // get the original event
      if (calType == Calendar.TYPE_PRIVATE)
        originalEvent = getUserEvent(username, fromCalendar, eventId);
      else if (calType == Calendar.TYPE_PUBLIC)
        originalEvent = getGroupEvent(fromCalendar, eventId);
      else if (calType == Calendar.TYPE_SHARED)
        originalEvent = getSharedEvent(username, fromCalendar, eventId);
      // update original event from occurrence
      java.util.Calendar fromDate = Utils.getInstanceTempCalendar();
      fromDate.setTime(originalEvent.getFromDateTime());
      java.util.Calendar newFromDate = Utils.getInstanceTempCalendar();
      newFromDate.setTime(occurrence.getFromDateTime());
      fromDate.set(java.util.Calendar.HOUR_OF_DAY, newFromDate.get(java.util.Calendar.HOUR_OF_DAY));
      fromDate.set(java.util.Calendar.MINUTE, newFromDate.get(java.util.Calendar.MINUTE));
      originalEvent.setFromDateTime(fromDate.getTime());

      // calculate time amount
      java.util.Calendar newToDate = Utils.getInstanceTempCalendar();
      newToDate.setTime(occurrence.getToDateTime());
      int diffMinutes = (int) (newToDate.getTimeInMillis() - newFromDate.getTimeInMillis())
          / (60 * 1000);

      newToDate.setTime(fromDate.getTime());
      newToDate.add(java.util.Calendar.MINUTE, diffMinutes);
      originalEvent.setToDateTime(newToDate.getTime());

      originalEvent.setSummary(occurrence.getSummary());
      originalEvent.setDescription(occurrence.getDescription());
      originalEvent.setEventCategoryId(occurrence.getEventCategoryId());
      originalEvent.setEventCategoryName(occurrence.getEventCategoryName());
      originalEvent.setMessage(occurrence.getMessage());
      originalEvent.setLocation(occurrence.getLocation());
      List<Attachment> attachments = occurrence.getAttachment();
      originalEvent.setAttachment(attachments);
      originalEvent.setInvitation(occurrence.getInvitation());
      originalEvent.setParticipant(occurrence.getParticipant());
      originalEvent.setParticipantStatus(occurrence.getParticipantStatus());
      originalEvent.setReminders(occurrence.getReminders());
      originalEvent.setSendOption(occurrence.getSendOption());
      originalEvent.setStatus(occurrence.getStatus());
      originalEvent.setPriority(occurrence.getPriority());
      originalEvent.setRepeatType(occurrence.getRepeatType());
      originalEvent.setRepeatUntilDate(occurrence.getRepeatUntilDate());
      originalEvent.setRepeatCount(occurrence.getRepeatCount());
      originalEvent.setRepeatInterval(occurrence.getRepeatInterval());
      originalEvent.setRepeatByDay(occurrence.getRepeatByDay());
      originalEvent.setRepeatByMonthDay(occurrence.getRepeatByMonthDay());

      // if move occurrence to another calendar, same date
      if (!fromCalendar.equals(toCalendar)) {
        // remove original event from old calendar
        if (calType == Calendar.TYPE_PRIVATE)
          removeUserEvent(username, fromCalendar, eventId);
        else if (calType == Calendar.TYPE_PUBLIC)
          removePublicEvent(fromCalendar, eventId);
        else if (calType == Calendar.TYPE_SHARED)
          removeSharedEvent(username, fromCalendar, eventId);

        // save new original event to new calendar
        CalendarEvent newEvent = new CalendarEvent(originalEvent);
        newEvent.setCalendarId(toCalendar);
        newEvent.setExcludeId(originalEvent.getExcludeId());

        if (toCalType == Calendar.TYPE_PRIVATE)
          saveUserEvent(username, toCalendar, newEvent, true);
        else if (toCalType == Calendar.TYPE_SHARED)
          saveEventToSharedCalendar(username, toCalendar, newEvent, true);
        else if (toCalType == Calendar.TYPE_PUBLIC)
          savePublicEvent(toCalendar, newEvent, true);
      } else {
        // save original event
        if (calType == Calendar.TYPE_PRIVATE)
          saveUserEvent(username, toCalendar, originalEvent, false);
        else if (calType == Calendar.TYPE_SHARED)
          saveEventToSharedCalendar(username, toCalendar, originalEvent, false);
        else if (calType == Calendar.TYPE_PUBLIC)
          savePublicEvent(toCalendar, originalEvent, false);
      }
    } catch (Exception e) {
      log.error("Error when update recurrence series", e);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void confirmInvitation(String fromUserId,
                                String toUserId,
                                int calType,
                                String calendarId,
                                String eventId,
                                int answer) {
    try {
      Map<String, String> pars = new HashMap<String, String>();
      CalendarEvent event = getInvitationEvent(calType, calendarId, eventId, fromUserId);
      if (event != null) {
        if (event.getParticipant() != null) {
          for (String id : event.getParticipant()) {
            pars.put(id, id);
          }
        }
        for (String s : toUserId.split(",")) {
          if (Utils.DENY == answer) {

            pars.remove(s);
          }
          if (Utils.ACCEPT == answer || Utils.NOTSURE == answer) {
            pars.put(s, s);
          }
        }
        event.setParticipant(pars.values().toArray(new String[pars.values().size()]));
        if (Calendar.TYPE_PRIVATE == calType) {
          saveUserEvent(fromUserId, calendarId, event, false);
        } else if (Calendar.TYPE_SHARED == calType) {
          saveEventToSharedCalendar(fromUserId, calendarId, event, false);
        } else if (Calendar.TYPE_PUBLIC == calType) {
          savePublicEvent(calendarId, event, false);
        }
      }
    } catch (Exception e) {
      log.error(String.format("Failed to confirm an invitation from user %s to user %s with event %s in calendar %s",
                              fromUserId,
                              toUserId,
                              eventId,
                              calendarId),
                              e);
    }
  }

  private CalendarEvent getInvitationEvent(int calType,
                                           String calendarId,
                                           String eventId,
                                           String fromUserId) throws Exception {
    CalendarEvent event = null;
    if (Calendar.TYPE_PRIVATE == calType) {
      event = getUserEvent(fromUserId, calendarId, eventId);
    } else if (Calendar.TYPE_SHARED == calType) {
      List<String> calendarIds = new ArrayList<String>();
      calendarIds.add(calendarId);
      for (CalendarEvent calEvent : getSharedEventByCalendars(fromUserId, calendarIds)) {
        if (calEvent.getId().equals(eventId)) {
          event = calEvent;
          break;
        }
      }
    } else if (Calendar.TYPE_PUBLIC == calType) {
      event = getGroupEvent(calendarId, eventId);
    }
    return event;
  }

  /**
   * {@inheritDoc}
   */
  public void confirmInvitation(String fromUserId,
                                String confirmingEmail,
                                String confirmingUser,
                                int calType,
                                String calendarId,
                                String eventId,
                                int answer) throws Exception {
    try {
      Map<String, String> pars = new HashMap<String, String>();
      CalendarEvent event = getInvitationEvent(calType, calendarId, eventId, fromUserId);
      if (event != null) {
        if (event.getParticipantStatus() != null) {
          for (String parStatus : event.getParticipantStatus()) {
            String[] entry = parStatus.split(":");
            if (entry.length > 1)
              pars.put(entry[0], entry[1]);
            else
              pars.put(entry[0], Utils.EMPTY_STR);
          }
        }
        String status = Utils.EMPTY_STR;
        switch (answer) {
        case Utils.DENY:
          status = Utils.STATUS_NO;
          break;
        case Utils.ACCEPT:
          status = Utils.STATUS_YES;
          break;
        case Utils.NOTSURE:
          status = Utils.STATUS_MAYBE;
          break;
        default:
          break;
        }

        if (pars.containsKey(confirmingUser)) {
          pars.remove(confirmingUser);
          pars.put(confirmingUser, status);
        }
        if (pars.containsKey(confirmingEmail)) {
          pars.remove(confirmingEmail);
          pars.put(confirmingEmail, status);
        }
        Map<String, String> participant = new HashMap<String, String>();
        for (Entry<String, String> par : pars.entrySet()) {
          participant.put(par.getKey() + ":" + par.getValue(), Utils.EMPTY_STR);
        }
        event.setParticipantStatus(participant.keySet().toArray(new String[participant.keySet()
                                                                           .size()]));
        if (Calendar.TYPE_PRIVATE == calType) {
          saveUserEvent(fromUserId, calendarId, event, false);
        } else if (Calendar.TYPE_SHARED == calType) {
          saveEventToSharedCalendar(fromUserId, calendarId, event, false);
        } else if (Calendar.TYPE_PUBLIC == calType) {
          savePublicEvent(calendarId, event, false);
        }
      }

    } catch (Exception e) {
      throw new Exception(e.getClass().toString(), e.fillInStackTrace());
    }
  }

  /**
   * {@inheritDoc}
   */
  public int getTypeOfCalendar(String userName, String calendarId) {
    Calendar cal = calendarCache.get(this, new KeyValuePair(calendarId, userName));
    if(isNullCalendar(cal)) {
      cal = getCalendar(calendarId, null);
      if(cal == null) {
        cal = getCalendar(calendarId, userName);
        if(cal == null) {
          return Utils.INVALID_TYPE;
        }
      }
    }
    if(!cal.isCalTypeChecked()) {
      int calType = getTypeOfCalendarFromStore(calendarId, userName);
      if(calType == Utils.INVALID_TYPE) {
        return Utils.INVALID_TYPE;
      }
      cal.setCalType(calType);
      cal.setCalTypeChecked(true);
    }
    return cal.getCalType();
  }

  private boolean isNullCalendar(Calendar cal) {
    return cal == null || cal == Calendar.NULL_OBJECT || cal.getId() == null;
  }

  public int getTypeOfCalendarFromStore(String calendarId, String userName) {
    int calType = Utils.INVALID_TYPE;
    try {
      getUserCalendarHome(userName).getNode(calendarId);
      calType = Utils.PRIVATE_TYPE;
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("Failed to find calendar %s from user node of user %s",
                                calendarId,
                                userName), e);
      }
    }
    try {
      getPublicCalendarHome().getNode(calendarId);
      calType = Utils.PUBLIC_TYPE;
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("Failed to find calendar %s from public node of user %s",
                                calendarId,
                                userName), e);
      }
    }
    try {
      Node sharedCalendarHome = getSharedCalendarHome();
      if (sharedCalendarHome.hasNode(userName)) {
        Node userNode = sharedCalendarHome.getNode(userName);
        PropertyIterator iter = userNode.getReferences();
        Node calendar;
        while (iter.hasNext() && calType == Utils.INVALID_TYPE) {
          calendar = iter.nextProperty().getParent();
          if (calendar.getProperty(Utils.EXO_ID).getString().equals(calendarId)) {
            calType = Utils.SHARED_TYPE;
          }
        }
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("Failed to find calendar %s from shared node of user %s",
                                calendarId,
                                userName), e);
      }
    }
    return calType;
  }

  /**
   * {@inheritDoc}
   */
  public SessionProvider createSessionProvider() {
    SessionProvider provider = sessionProviderService_.getSessionProvider(null);
    if (provider == null) {
      log.info("No user session provider was available, trying to use a system session provider");
      provider = sessionProviderService_.getSystemSessionProvider(null);
    }
    return SessionProvider.createSystemProvider();
  }

  /**
   * {@inheritDoc}
   */
  @SuppressWarnings("unused")
  public SessionProvider createUserProvider() {
    return  sessionProviderService_.getSessionProvider(null);
  }

  /**
   * {@inheritDoc}
   */
  public SessionProvider createSystemProvider() {
    return sessionProviderService_.getSystemSessionProvider(null);
  }

  /**
   * {@inheritDoc}
   */
  @SuppressWarnings("unused")
  public void closeSessionProvider(SessionProvider sessionProvider) {
    if (sessionProvider != null) {
      sessionProvider.close();
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getNodeByPath(String nodePath, SessionProvider sessionProvider) throws Exception {
    return (Node) getSession(sessionProvider).getItem(nodePath);
  }

  /**
   * {@inheritDoc}
   */
  public Session getSession(SessionProvider sprovider) throws Exception {
    ManageableRepository currentRepo = repoService_.getCurrentRepository();
    return sprovider.getSession(currentRepo.getConfiguration().getDefaultWorkspaceName(),
                                currentRepo);
  }

  public Session getSystemSession() throws Exception {
    return getSession(createSystemProvider());
  }
  private String[] ValuesToStrings(Value[] Val) throws Exception {
    if (Val.length == 1)
      return new String[] { Val[0].getString() };
    String[] Str = new String[Val.length];
    for (int i = 0; i < Val.length; ++i) {
      Str[i] = Val[i].getString();
    }
    return Str;
  }

  public void autoShareCalendar(List<String> groupsOfUser, String receiver) throws Exception {
    Node sharedHome = getSharedCalendarHome();
    NodeIterator userNodes = sharedHome.getNodes();
    List<String> sharedCalendars = new ArrayList<String>();
    while (userNodes.hasNext()) {
      Node sharedNode = userNodes.nextNode();
      PropertyIterator iter = sharedNode.getReferences();
      while (iter.hasNext()) {
        try {
          Node calendarNode = iter.nextProperty().getParent();
          String calendarId = calendarNode.getProperty(Utils.EXO_ID).getString();
          if (!sharedCalendars.contains(calendarId)) {
            sharedCalendars.add(calendarId);
            Value[] viewPers = calendarNode.getProperty(Utils.EXO_VIEW_PERMISSIONS).getValues();
            for (Value viewPer : viewPers) {
              for (String groupId : groupsOfUser) {
                if (viewPer.getString().equals(groupId)) {
                  Node sharedCalendarHome = getSharedCalendarHome();
                  Value[] values = {};
                  if (calendarNode.isNodeType(Utils.EXO_SHARED_MIXIN)) {
                    values = calendarNode.getProperty(Utils.EXO_SHARED_ID).getValues();
                  } else {
                    calendarNode.addMixin(Utils.EXO_SHARED_MIXIN);
                  }
                  List<Value> valueList = new ArrayList<Value>();
                  for (int i = 0; i < values.length; i++) {
                    Value value = values[i];
                    valueList.add(value);
                  }
                  valueList = calculateSharedCalendar(receiver,
                                                      calendarNode,
                                                      values,
                                                      valueList,
                                                      sharedCalendarHome);
                  if (valueList.size() > 0) {
                    calendarNode.setProperty(Utils.EXO_SHARED_ID,
                                             valueList.toArray(new Value[valueList.size()]));
                    calendarNode.save();
                    sharedCalendarHome.getSession().save();
                    calendarNode.getSession().save();

                    String username = null;
                    if(calendarNode.hasProperty(Utils.EXO_CALENDAR_OWNER)) {
                      username = calendarNode.getProperty(Utils.EXO_CALENDAR_OWNER).getString();
                    }
                    clearCalendarCache(calendarId, username);
                  }
                }
              }
            }
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Exception in method autoShareCalendar", e);
          }
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public void removeSharedCalendar(String username, String calendarId) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      String uuid = userNode.getProperty("jcr:uuid").getString();
      PropertyIterator iter = userNode.getReferences();
      Node calendar;
      CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));
      Map<String, String> map = new HashMap<String, String>();
      for (String key : calSetting.getSharedCalendarsColors()) {
        map.put(key.split(":")[0], key.split(":")[1]);
      }
      while (iter.hasNext()) {
        calendar = iter.nextProperty().getParent();
        if (calendar.getProperty(Utils.EXO_ID).getString().equals(calendarId)) {
          deleteSharedCalendar(username, uuid, calSetting, calendar, map);

          List<String> viewPerms = new ArrayList<String>();
          if (calendar.hasProperty(Utils.EXO_VIEW_PERMISSIONS)) {
            Value[] viewValues = calendar.getProperty(Utils.EXO_VIEW_PERMISSIONS).getValues();
            for (Value v : viewValues) {
              if (v.getString() != null && !v.getString().equals(username))
                viewPerms.add(v.getString());
            }
          }
          List<String> editPerms = new ArrayList<String>();
          if (calendar.hasProperty(Utils.EXO_EDIT_PERMISSIONS)) {
            Value[] editValues = calendar.getProperty(Utils.EXO_EDIT_PERMISSIONS).getValues();
            for (Value v : editValues) {
              if (v.getString() != null && !v.getString().equals(username))
                editPerms.add(v.getString());
            }
          }
          calendar.setProperty(Utils.EXO_VIEW_PERMISSIONS,
                               viewPerms.toArray(new String[viewPerms.size()]));
          calendar.setProperty(Utils.EXO_EDIT_PERMISSIONS,
                               editPerms.toArray(new String[editPerms.size()]));
          calendar.getSession().save();
          calendar.refresh(true);

          clearCalendarCache(calendarId, username);
          break;
        }
      }
      try {
        removeFeed(username, calendarId);
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.debug("Fail to remove feed", e);
        }
      }
    }
  }

  public void removeSharedCalendarFolder(String username) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      String uuid = userNode.getProperty("jcr:uuid").getString();
      PropertyIterator propIter = userNode.getReferences();
      while (propIter.hasNext()) {
        Property prop = propIter.nextProperty();
        Node calendar = prop.getParent();
        Value[] sharedIds = prop.getValues();
        List<Value> newValues = new ArrayList<Value>();
        for (Value value : sharedIds) {
          if (!value.getString().equals(uuid)) {
            newValues.add(value);
          }
        }
        calendar.setProperty(Utils.EXO_SHARED_ID, newValues.toArray(new Value[newValues.size()]));
        newValues.clear();

        if (calendar.hasProperty(Utils.EXO_VIEW_PERMISSIONS)) {
          Value[] viewPerms = calendar.getProperty(Utils.EXO_VIEW_PERMISSIONS).getValues();
          for (Value v : viewPerms) {
            if (!username.equals(v.getString())) {
              newValues.add(v);
            }
          }
          calendar.setProperty(Utils.EXO_VIEW_PERMISSIONS,
                               newValues.toArray(new Value[newValues.size()]));
          newValues.clear();
        }

        if (calendar.hasProperty(Utils.EXO_EDIT_PERMISSIONS)) {
          Value[] editPerms = calendar.getProperty(Utils.EXO_EDIT_PERMISSIONS).getValues();
          for (Value v : editPerms) {
            if (!username.equals(v.getString())) {
              newValues.add(v);
            }
          }
          calendar.setProperty(Utils.EXO_EDIT_PERMISSIONS,
                               newValues.toArray(new Value[newValues.size()]));
        }
        calendar.save();
        clearCalendarCache(calendar.getProperty(Utils.EXO_ID).getString(), username);
      }
      userNode.remove();
      sharedCalendarHome.save();
    }
  }

  public void autoRemoveShareCalendar(String groupId, String username) throws Exception {
    Node sharedCalendarHome = getSharedCalendarHome();
    if (sharedCalendarHome.hasNode(username)) {
      Node userNode = sharedCalendarHome.getNode(username);
      String uuid = userNode.getProperty("jcr:uuid").getString();
      PropertyIterator iter = userNode.getReferences();
      CalendarSetting calSetting = getCalendarSetting(getUserCalendarServiceHome(username));
      Node calendar;
      OrganizationService organizationService = (OrganizationService) ExoContainerContext.getCurrentContainer()
          .getComponentInstanceOfType(OrganizationService.class);
      Map<String, String> map = new HashMap<String, String>();
      for (String key : calSetting.getSharedCalendarsColors()) {
        map.put(key.split(":")[0], key.split(":")[1]);
      }
      while (iter.hasNext()) {
        calendar = iter.nextProperty().getParent();
        List<String> viewPers = new ArrayList<String>();
        for (Value value : calendar.getProperty(Utils.EXO_VIEW_PERMISSIONS).getValues()) {
          viewPers.add(value.getString());
        }
        viewPers.remove(groupId);
        boolean deleteShared = true;
        if (viewPers.contains(username))
          deleteShared = false;
        else {
          Object[] groups = organizationService.getGroupHandler()
              .findGroupsOfUser(username)
              .toArray();
          for (Object object : groups) {
            if (viewPers.contains(((Group) object).getId())) {
              deleteShared = false;
              break;
            }
          }
        }
        if (deleteShared) {
          for (Value viewPer : calendar.getProperty(Utils.EXO_VIEW_PERMISSIONS).getValues())
            if (viewPer.getString().equals(groupId)) {
              deleteSharedCalendar(username, uuid, calSetting, calendar, map);

              calendar.getSession().save();
              clearCalendarCache(calendar.getProperty(Utils.EXO_ID).getString(), username);
              calendar.refresh(true);
              break;
            }
          try {
            removeFeed(username, calendar.getProperty(Utils.EXO_ID).getString());
          } catch (Exception e) {
            if (log.isDebugEnabled()) {
              log.debug("Fail to remove feed", e);
            }
          }
        }
      }
    }
  }

  private void deleteSharedCalendar(String username,
                                    String userNodeUuid,
                                    CalendarSetting calSetting,
                                    Node calendarNode,
                                    Map<String, String> colorMap) throws Exception {
    colorMap.remove(calendarNode.getProperty(Utils.EXO_ID).getString());

    Value[] values = calendarNode.getProperty(Utils.EXO_SHARED_ID).getValues();
    List<Value> newValues = new ArrayList<Value>();
    for (Value value : values) {
      if (!value.getString().equals(userNodeUuid)) {
        newValues.add(value);
      }
    }
    List<String> calColors = new ArrayList<String>();
    for (String key : colorMap.keySet()) {
      calColors.add(key + Utils.COLON + colorMap.get(key));
    }
    calSetting.setSharedCalendarsColors(calColors.toArray(new String[calColors.size()]));
    saveCalendarSetting(calSetting, username);
    calendarNode.setProperty(Utils.EXO_SHARED_ID, newValues.toArray(new Value[newValues.size()]));

    clearCalendarCache(calendarNode.getProperty(Utils.EXO_ID).getString(), username);
  }

  public void assignGroupTask(String taskId, String calendarId, String assignee) throws Exception {
    Node calendarNode = getPublicCalendarHome().getNode(calendarId);
    Node eventNode = calendarNode.getNode(taskId);
    StringBuilder taskDelegator = new StringBuilder();
    if (eventNode.hasProperty(Utils.EXO_TASK_DELEGATOR))
      taskDelegator.append(eventNode.getProperty(Utils.EXO_TASK_DELEGATOR).getString());
    if (assignee != null && assignee.length() > 0) {
      if (taskDelegator.toString().trim().length() == 0) {
        taskDelegator = new StringBuilder(assignee);
      } else {
        taskDelegator.append(",").append(assignee);
      }
      eventNode.setProperty(Utils.EXO_TASK_DELEGATOR, taskDelegator.toString());
      eventNode.getSession().save();
    }
  }

  public void setGroupTaskStatus(String taskId, String calendarId, String status) throws Exception {
    Node calendarNode = getPublicCalendarHome().getNode(calendarId);
    Node eventNode = calendarNode.getNode(taskId);
    if (status != null && status.length() > 0) {
      eventNode.setProperty(Utils.EXO_EVENT_STATE, status);
      eventNode.getSession().save();
    }
  }

  public boolean isRemoteCalendar(String username, String calendarId) throws Exception {
    try {
      Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
      if (calendarNode.isNodeType(Utils.EXO_REMOTE_MIXIN))
        return true;
      return false;
    } catch (Exception e) {
      log.debug(e.getMessage());
      return false;
    }
  }

  @Override
  public Calendar updateRemoteCalendarInfo(RemoteCalendar remoteCalendar) throws Exception {
    Node calendarNode = getUserCalendarHome(remoteCalendar.getUsername()).getNode(remoteCalendar.getCalendarId());
    updateRemoteCalendarInfo(calendarNode, remoteCalendar);
    return getUserCalendar(remoteCalendar.getUsername(), remoteCalendar.getCalendarId());
  }

  private void updateRemoteCalendarInfo(Node calendarNode, RemoteCalendar remoteCalendar) throws Exception {
    calendarNode.setProperty(Utils.EXO_REMOTE_TYPE, remoteCalendar.getType());
    calendarNode.setProperty(Utils.EXO_DESCRIPTION, remoteCalendar.getDescription());
    calendarNode.setProperty(Utils.EXO_REMOTE_URL, remoteCalendar.getRemoteUrl());
    calendarNode.setProperty(Utils.EXO_REMOTE_SYNC_PERIOD, remoteCalendar.getSyncPeriod());
    calendarNode.setProperty(Utils.EXO_REMOTE_USERNAME, (remoteCalendar.getRemoteUser()));
    calendarNode.setProperty(Utils.EXO_REMOTE_PASSWORD, (remoteCalendar.getRemotePassword()));
    calendarNode.setProperty(Utils.EXO_REMOTE_AFTER_DATE, (remoteCalendar.getAfterDateSave()));
    calendarNode.setProperty(Utils.EXO_REMOTE_BEFORE_DATE, (remoteCalendar.getBeforeDateSave()));
    if (remoteCalendar.getLastUpdated() != null)
      calendarNode.setProperty(Utils.EXO_REMOTE_LAST_UPDATED, remoteCalendar.getLastUpdated());
    else if (!calendarNode.hasProperty(Utils.EXO_REMOTE_LAST_UPDATED))
      calendarNode.setProperty(Utils.EXO_REMOTE_LAST_UPDATED, Utils.getInstanceTempCalendar());
    calendarNode.save();
  }

  public RemoteCalendar getRemoteCalendar(String username, String remoteCalendarId) throws Exception {
    RemoteCalendar remoteCalendar = new RemoteCalendar();
    try {
      Node calendarNode = getUserCalendarHome(username).getNode(remoteCalendarId);
      remoteCalendar.setUsername(username);
      remoteCalendar.setCalendarId(remoteCalendarId);
      StringBuilder namePattern = new StringBuilder(128);
      namePattern.append(Utils.EXO_REMOTE_TYPE).append('|').append(Utils.EXO_REMOTE_SYNC_PERIOD).append('|')
      .append(Utils.EXO_REMOTE_BEFORE_DATE).append('|').append(Utils.EXO_REMOTE_AFTER_DATE).append('|')
      .append(Utils.EXO_REMOTE_URL).append('|').append(Utils.EXO_REMOTE_USERNAME).append('|')
      .append(Utils.EXO_REMOTE_PASSWORD).append('|').append(Utils.EXO_REMOTE_LAST_UPDATED);
      PropertyIterator it = calendarNode.getProperties(namePattern.toString());
      while (it.hasNext()) {
        Property p = it.nextProperty();
        String name = p.getName();
        if (name.equals(Utils.EXO_REMOTE_TYPE)) {
          remoteCalendar.setType(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_SYNC_PERIOD)) {
          remoteCalendar.setSyncPeriod(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_BEFORE_DATE)) {
          remoteCalendar.setBeforeDateSave(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_AFTER_DATE)) {
          remoteCalendar.setAfterDateSave(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_URL)) {
          remoteCalendar.setRemoteUrl(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_USERNAME)) {
          remoteCalendar.setRemoteUser(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_PASSWORD)) {
          remoteCalendar.setRemotePassword(p.getString());
        } else if (name.equals(Utils.EXO_REMOTE_LAST_UPDATED)) {
          remoteCalendar.setLastUpdated(p.getDate());
        }
      }
    } catch (Exception e) {
      log.debug("Failed to get RemoteCalendar by user.", e);
    }
    return remoteCalendar;
  }

  public Calendar createRemoteCalendar(RemoteCalendar remoteCalendar) throws Exception {
    Calendar eXoCalendar = new Calendar();
    eXoCalendar.setName(remoteCalendar.getCalendarName());
    eXoCalendar.setPublic(false);
    eXoCalendar.setCalendarOwner(remoteCalendar.getUsername());
    eXoCalendar.setDescription(remoteCalendar.getDescription());
    eXoCalendar.setCalendarColor(remoteCalendar.getCalendarColor());
    saveUserCalendar(remoteCalendar.getUsername(), eXoCalendar, true);

    // add mixin type exo:remoteCalendar to this calendar
    Node calendarNode = getUserCalendarHome(remoteCalendar.getUsername()).getNode(eXoCalendar.getId());
    if (!calendarNode.isNodeType(Utils.EXO_REMOTE_MIXIN)) {
      calendarNode.addMixin(Utils.EXO_REMOTE_MIXIN);
    }
    updateRemoteCalendarInfo(calendarNode, remoteCalendar);
    return eXoCalendar;
  }

  public void setRemoteEvent(String username,
                             String calendarId,
                             String eventId,
                             String href,
                             String etag) throws Exception {
    Node eventNode = getUserCalendarHome(username).getNode(calendarId).getNode(eventId);
    if (!eventNode.isNodeType(Utils.EXO_REMOTE_EVENT_MIXIN)) {
      eventNode.addMixin(Utils.EXO_REMOTE_EVENT_MIXIN);
    }
    if (href != null) {
      eventNode.setProperty(Utils.EXO_CALDAV_HREF, href);
    }
    eventNode.setProperty(Utils.EXO_CALDAV_ETAG, etag);
    eventNode.save();
  }

  @Override
  public void setRemoteCalendarLastUpdated(String username,
                                           String calendarId,
                                           java.util.Calendar timeGMT) throws Exception {
    Node calendarNode = getUserCalendarHome(username).getNode(calendarId);
    calendarNode.setProperty(Utils.EXO_REMOTE_LAST_UPDATED, timeGMT);
    calendarNode.save();

    clearCalendarCache(calendarNode.getProperty(Utils.EXO_ID).getString(), username);
  }

  public Calendar getRemoteCalendar(String username, String remoteUrl, String remoteType) throws Exception {
    try {
      Node calendarHome = getUserCalendarHome(username);
      String queryString = new StringBuilder("/jcr:root" + XPathUtils.escapeIllegalXPathName(calendarHome.getPath())
                                             + "//element(*,exo:remoteCalendar)[@exo:remoteUrl='").append(remoteUrl)
                                             .append("' and @exo:remoteType='")
                                             .append(remoteType)
                                             .append("']")
                                             .toString();
      QueryManager queryManager = calendarHome.getSession().getWorkspace().getQueryManager();
      Query query = queryManager.createQuery(queryString.toString(), Query.XPATH);
      QueryResult results = query.execute();
      NodeIterator iter = results.getNodes();
      if (iter.hasNext()) {
        Node calNode = iter.nextNode();
        Calendar calendar = getCalendar(null, username, calNode, true);
        return calendar;
      }
      return null;
    } catch (Exception e) {
      return null;
    }
  }

  public int getRemoteCalendarCount(String username) throws Exception {
    try {
      Node calendarHome = getUserCalendarHome(username);
      String queryString = new StringBuilder("/jcr:root").append(XPathUtils.escapeIllegalXPathName(calendarHome.getPath()))
          .append("//element(*,exo:remoteCalendar)").toString();
      QueryManager queryManager = calendarHome.getSession().getWorkspace().getQueryManager();
      Query query = queryManager.createQuery(queryString.toString(), Query.XPATH);
      QueryResult results = query.execute();
      NodeIterator iter = results.getNodes();
      int count = 0;
      while (iter.hasNext()) {
        iter.nextNode();
        count++;
      }
      return count;
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e);
      return 0;
    }
  }

  /*
   * (non-Javadoc)
   * @see
   * org.exoplatform.calendar.service.DataStorage#setCalDavResourceHref(java
   * .lang.String, java.lang.String, java.lang.String, java.lang.String)
   */
  @Override
  public void setCalDavResourceHref(String username, String calendarId, String eventId, String href) throws Exception {
    Node eventNode = getUserCalendarHome(username).getNode(calendarId).getNode(eventId);
    eventNode.setProperty(Utils.EXO_CALDAV_HREF, href);
  }

  /*
   * (non-Javadoc)
   * @see
   * org.exoplatform.calendar.service.DataStorage#setCalDavResourceEtag(java
   * .lang.String, java.lang.String, java.lang.String, java.lang.String)
   */
  @Override
  public void setCalDavResourceEtag(String username, String calendarId, String eventId, String etag) throws Exception {
    Node eventNode = getUserCalendarHome(username).getNode(calendarId).getNode(eventId);
    eventNode.setProperty(Utils.EXO_CALDAV_HREF, etag);
  }

  public CalendarEvent getEventById(String eventId) throws Exception {
    CalendarEvent event = eventCache.get(this, eventId);
    return event.equals(CalendarEvent.NULL_OBJECT )|| event.getCalendarId() == null ? null : event;
  }

  @Override
  public Calendar getCalendarById(String calId) throws Exception {
    return getCalendar(calId, null);
  }

  public NodeHierarchyCreator getNodeHierarchyCreator() {
    return nodeHierarchyCreator_;
  }

  private static class CalendarSettingLoader implements Loader<String, CalendarSetting, JCRDataStorage> {
    @Override
    public CalendarSetting retrieve(JCRDataStorage context, String key) throws Exception {
      Node calendarHome = context.getUserCalendarServiceHome(key);
      if (!calendarHome.hasNode(CALENDAR_SETTING)) {
        return null;
      }
      return context.getCalendarSetting(calendarHome);
    }
  };
  
  private static class UserCalendarLoader implements Loader<String, List<Calendar>, JCRDataStorage> {
    @Override
    public List<Calendar> retrieve(JCRDataStorage context, String key) throws Exception {
      List<Calendar> calList  = new LinkedList<Calendar>();
      Node userCalendarHome = context.getUserCalendarHome(key);
      NodeIterator iter     = userCalendarHome.getNodes();
      while (iter.hasNext()) {
        calList.add(context.loadCalendar(iter.nextNode()));
      }
      return calList;
    }
  };

  private static class CalendarLoader implements Loader<KeyValuePair, Calendar, JCRDataStorage> {
    @Override
    public Calendar retrieve(JCRDataStorage context, KeyValuePair calKey) throws Exception {
      String calendarId = calKey.getKey();
      String username = calKey.getValue();

      if(StringUtils.isNotBlank(username)) {
        if(context.getUserCalendarHome(username).hasNode(calendarId)) {
          Node calendarNode = context.getUserCalendarHome(username).getNode(calendarId);
          return context.getCalendar(new String[] { calendarId }, username, calendarNode, true);
        } else {
          return Calendar.NULL_OBJECT;
        }
      }

      if(context.getPublicCalendarHome().hasNode(calendarId)) {
        Node calendarNode = context.getPublicCalendarHome().getNode(calendarId);
        Calendar loadedCalendar = context.getCalendar(new String[] { calendarId }, null, calendarNode, true);
        if(loadedCalendar != null) {
          loadedCalendar.setCalType(Utils.PUBLIC_TYPE);
          loadedCalendar.setCalTypeChecked(true);
        }
        return loadedCalendar;
      }

      Node calendarApp = context.getNodeHierarchyCreator().getPublicApplicationNode(Utils.createSystemProvider());
      QueryManager queryManager = calendarApp.getSession().getWorkspace().getQueryManager();
      String sql = "select * from exo:calendar where exo:id=" + "\'" + calendarId + "\'";
      Query query = queryManager.createQuery(sql, Query.SQL);
      QueryResult result = query.execute();
      NodeIterator nodesIt = result.getNodes();
      if(nodesIt.hasNext()) {
        return context.loadCalendar(nodesIt.nextNode());
      } else {
        return Calendar.NULL_OBJECT;
      }
    }
  };
  
  private static class GroupCalendarLoader implements Loader<String, List<Calendar>, JCRDataStorage> {
    @Override
    public List<Calendar> retrieve(JCRDataStorage context, String key) throws Exception {
      Node calendarHome = context.getPublicCalendarHome();
      QueryManager qm = calendarHome.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(key, Query.XPATH);
      QueryResult result = query.execute();
      
      List<Calendar> calendarList = new LinkedList<Calendar>();
      NodeIterator it = result.getNodes();
      while (it.hasNext()) {
        Node calNode = it.nextNode();
        Calendar cal = context.loadCalendar(calNode);
        calendarList.add(cal);
      }
      return calendarList;
    }
  };
  
  private static class CalendarEventLoader implements Loader<String, CalendarEvent, JCRDataStorage> {
      @Override
      public CalendarEvent retrieve(JCRDataStorage context, String key) throws Exception {
        SessionProvider provider = null;
        try {
          provider = context.createSessionProvider();
          QueryManager queryManager = context.getSession(provider).getWorkspace().getQueryManager();
          String sql = "select * from exo:calendarEvent where exo:id=" + "\'" + key + "\'";
          Query query = queryManager.createQuery(sql, Query.SQL);
          QueryResult result = query.execute();
          NodeIterator nodesIt = result.getNodes();
          if (nodesIt.hasNext()) {
            return context.getEvent(nodesIt.nextNode());
          } else {
            return CalendarEvent.NULL_OBJECT;
          }
        } finally {
          if (provider != null) {
            provider.close();
          }
        }
      }
    };
  
  /**
   * This kind of locks can self unregister from the map of locks
   */
  private class InternalLock extends ReentrantLock {

    /**
     * Serial Version UID
     */
    private static final long serialVersionUID = -3362387346368015145L;

    /**
     * The id corresponding to the lock in the map
     */
    private final String fullId;

    /**
     * The default constructor
     * @param fullId the id corresponding to the lock in the map
     */
    public InternalLock(String fullId) {
      super();
      this.fullId = fullId;
    }

    @Override
    public void unlock() {
      if (!hasQueuedThreads()) {
        // No thread is currently waiting for this lock
        // The lock will then be removed
        locks.remove(fullId, this);
      }
      super.unlock();
    }
  }


  @Override
  public Collection<CalendarEvent> getAllExcludedEvent(CalendarEvent originEvent,java.util.Calendar from, java.util.Calendar to, String userId) {
    Collection<CalendarEvent> coll = new ArrayList<CalendarEvent>();
    Collection<String> calendarIds = Arrays.asList(new String[]{originEvent.getCalendarId()}); 
    CalendarSetting st = null;
    try {
      st = getCalendarSetting(userId);
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug(e);
    }
    Collection<String> filterCalendarIds = Arrays.asList(st.getFilterPrivateCalendars());
    filterCalendarIds.addAll(Arrays.asList(st.getFilterPublicCalendars()));
    filterCalendarIds.addAll(Arrays.asList(st.getFilterSharedCalendars()));

    StringBuffer queryString =  new StringBuffer("SELECT * FROM exo:calendarEvent WHERE ");
    queryString.append(" exo:originalReference ='").append(originEvent.getId()).append("'");
    QueryManager qm;
    try {
      qm = getUserCalendarHome(userId).getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryString.toString(), Query.SQL);
      QueryResult result = query.execute();
      NodeIterator it = result.getNodes();
      while (it.hasNext()) {
        Node eventNode = it.nextNode();
        CalendarEvent event = getEvent(eventNode);
        event.setCalType(originEvent.getCalType());
        coll.add(event);
      }
    } catch (RepositoryException e) {
      if (log.isDebugEnabled()) log.debug(e);
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug(e);
    }
    return coll;
  }


  @Override
  public Collection<CalendarEvent> buildSeriesByTime(CalendarEvent recurEvent,
                                                     java.util.Calendar from,
                                                     java.util.Calendar to,
                                                     String userId) {
    String timezone = TimeZone.getDefault().getID();
    try {
      timezone = getCalendarSetting(userId).getTimeZone();
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug(e);
    }
    if (Utils.isEmpty(recurEvent.getRepeatType()))
      return null;
    if (from.after(to) || !recurEvent.getFromDateTime().before(to.getTime())) {
      return null;
    }

    // check if this recurEvent is expired
    if (recurEvent.getRepeatUntilDate() != null
        && recurEvent.getRepeatUntilDate().before(from.getTime())) {
      return null;
    }

    TimeZone userTimeZone = DateUtils.getTimeZone(timezone);
    SimpleDateFormat format = new SimpleDateFormat(Utils.DATE_FORMAT_RECUR_ID);
    format.setTimeZone(userTimeZone);

    Collection<CalendarEvent> occurrences = new ArrayList<CalendarEvent>();

    int diffMinutes = (int) ((recurEvent.getToDateTime().getTime() - recurEvent.getFromDateTime()
        .getTime()) / (60 * 1000));

    Recur recur = null;
    try {
      recur = Utils.getICalendarRecur(recurEvent);
    } catch (Exception e) {
      if (log.isDebugEnabled()) log.debug(e);
    }
    if (recur == null)
      return null;

    DateTime ical4jEventFrom = new DateTime(recurEvent.getFromDateTime());//the date time of the first occurrence of the series
    net.fortuna.ical4j.model.TimeZone tz = null;
    try {
      tz = Utils.getICalTimeZone(DateUtils.getTimeZone(timezone));
    } catch (ParseException e) {
      if (log.isDebugEnabled()) log.debug(e);
    }
    ical4jEventFrom.setTimeZone(tz);

    Utils.adaptRepeatRule(recur, recurEvent.getFromDateTime(), CalendarService.PERSISTED_TIMEZONE, userTimeZone);

    java.util.Calendar occurenceFrom = java.util.Calendar.getInstance();
    occurenceFrom.setTime(from.getTime());

    // because occurrence event can begin before the 'from' but end after
    // 'from', so it still intersects with [from, to] window
    // thus, we decrease the 'from' value (by duration of event) to get such
    // occurrences
    occurenceFrom.add(java.util.Calendar.MINUTE, -(diffMinutes - 1));
    DateTime ical4jFrom = new DateTime(occurenceFrom.getTime());
    DateTime ical4jTo = new DateTime(to.getTime());
    Period period = new Period(ical4jFrom, ical4jTo);
    period.setTimeZone(tz);
    // get list of occurrences in a period
    DateList list = recur.getDates(ical4jEventFrom,
                                   period,
                                   net.fortuna.ical4j.model.parameter.Value.DATE_TIME);

    for (Object dt : list) {
      DateTime ical4jStart = (DateTime) dt;
      ical4jStart.setTimeZone(tz);

      // make occurrence
      CalendarEvent occurrence = new CalendarEvent(recurEvent);

      java.util.Calendar startTime = java.util.Calendar.getInstance(TimeZone.getDefault());
      java.util.Calendar endTime = java.util.Calendar.getInstance(TimeZone.getDefault());
      startTime.setTimeInMillis(ical4jStart.getTime());

      occurrence.setFromDateTime(startTime.getTime());

      endTime.setTime(startTime.getTime());
      endTime.add(java.util.Calendar.MINUTE, diffMinutes);

      occurrence.setToDateTime(endTime.getTime());

      // if user time zone uses DST, need to adapt the time occurrence 
      if( userTimeZone.useDaylightTime()) {
        adaptTimeToDST(occurrence, recurEvent, userTimeZone);    
      }

      String recurId = format.format(occurrence.getFromDateTime());

      // if this occurrence was listed in the exclude list, skip
      if (recurEvent.getExceptionIds() != null && recurEvent.getExceptionIds().contains(recurId))
        continue;

      occurrence.setRecurrenceId(recurId);
      occurrences.add(occurrence);
    }
    return occurrences;
  }

  public static class KeyValuePair extends SimpleEntry<String, String> {
    private static final long serialVersionUID = -2558703122631062086L;

    public KeyValuePair(String key, String value) {
      super(key, value);
    }
    
    @Override
    public boolean equals(Object o) {
      if(o  == null) {
        return false;
      }
      if(o instanceof KeyValuePair) {
        return hashCode() == o.hashCode();
      }
      return false;
    }

    @Override
    public int hashCode() {
      return (getKey()   == null ? 0 :   getKey().hashCode()) ^
          (getValue() == null ? 1 : getValue().hashCode());
    }
  }

  private class EventCalendarSelector implements CachedObjectSelector<String, CalendarEvent> {

    private String idCalendar;

    public EventCalendarSelector(String calId) {
      this.idCalendar = calId;
    }

    @Override
    public boolean select(String key, ObjectCacheInfo<? extends CalendarEvent> ocinfo) {
      CalendarEvent event = ocinfo.get();
      return !event.equals(CalendarEvent.NULL_OBJECT) && event.getCalendarId().equals(idCalendar);
    }

    @Override
    public void onSelect(ExoCache<? extends String, ? extends CalendarEvent> cache, String key, ObjectCacheInfo<? extends CalendarEvent> ocinfo) throws Exception {
      cache.remove(key);
    }
  }

  private class EventCategorySelector implements CachedObjectSelector<String, CalendarEvent> {

    private String idCategory;

    public EventCategorySelector(String idCategory) {
      this.idCategory = idCategory;
    }

    @Override
    public boolean select(String key, ObjectCacheInfo<? extends CalendarEvent> ocinfo) {
      CalendarEvent event = ocinfo.get();
      return event != null && event.getEventCategoryId().equals(idCategory);
    }

    @Override
    public void onSelect(ExoCache<? extends String, ? extends CalendarEvent> cache,
                         String key, ObjectCacheInfo<? extends CalendarEvent> ocinfo) throws Exception {
      cache.remove(key);
    }
  }

  private class GroupCalendarSelector implements CachedObjectSelector<String, List<Calendar>> {

    private Set<String> groups;

    public GroupCalendarSelector(Set<String> groups) {
      this.groups = groups;
    }

    @Override
    public boolean select(String key, ObjectCacheInfo<? extends List<Calendar>> ocinfo) {
      return !groups.isEmpty() && groups.contains(key);
    }

    @Override
    public void onSelect(ExoCache<? extends String, ? extends List<Calendar>> cache, String key,
                         ObjectCacheInfo<? extends List<Calendar>> ocinfo) throws Exception {
      cache.remove(key);
    }
  }

  private class GroupCalendarEventSelector implements CachedObjectSelector<String, List<CalendarEvent>> {

    private String pattern;

    public GroupCalendarEventSelector(String calendarPath) {
      StringBuilder queryEventsStatementSQL = new StringBuilder(" SELECT * FROM ").append("exo:calendarEvent")
              .append(" WHERE jcr:path LIKE '").append(calendarPath);
      this.pattern = queryEventsStatementSQL.toString();
    }

    @Override
    public boolean select(String key, ObjectCacheInfo<? extends List<CalendarEvent>> ocinfo) {
      return key.startsWith(pattern);
    }

    @Override
    public void onSelect(ExoCache<? extends String, ? extends List<CalendarEvent>> cache, String key,
                         ObjectCacheInfo<? extends List<CalendarEvent>> ocinfo) throws Exception {
      cache.remove(key);
    }
  }
}
