org.exoplatform.calendar.ws.CalendarRestApi.java Source code

Java tutorial

Introduction

Here is the source code for org.exoplatform.calendar.ws.CalendarRestApi.java

Source

/*
 * Copyright (C) 2003-2014 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.exoplatform.calendar.ws;

import javax.annotation.security.RolesAllowed;
import javax.jcr.query.Query;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.lang.StringUtils;
import org.exoplatform.calendar.service.Attachment;
import org.exoplatform.calendar.service.Calendar;
import org.exoplatform.calendar.service.Calendar.Type;
import org.exoplatform.calendar.service.CalendarCollection;
import org.exoplatform.calendar.service.CalendarEvent;
import org.exoplatform.calendar.service.CalendarImportExport;
import org.exoplatform.calendar.service.CalendarService;
import org.exoplatform.calendar.service.EventCategory;
import org.exoplatform.calendar.service.EventDAO;
import org.exoplatform.calendar.service.EventQuery;
import org.exoplatform.calendar.service.FeedData;
import org.exoplatform.calendar.service.GroupCalendarData;
import org.exoplatform.calendar.service.Invitation;
import org.exoplatform.calendar.service.RssData;
import org.exoplatform.calendar.service.Utils;
import org.exoplatform.calendar.service.impl.NewUserListener;
import org.exoplatform.calendar.ws.bean.AttachmentResource;
import org.exoplatform.calendar.ws.bean.CalendarResource;
import org.exoplatform.calendar.ws.bean.CategoryResource;
import org.exoplatform.calendar.ws.bean.CollectionResource;
import org.exoplatform.calendar.ws.bean.ErrorResource;
import org.exoplatform.calendar.ws.bean.EventResource;
import org.exoplatform.calendar.ws.bean.FeedResource;
import org.exoplatform.calendar.ws.bean.InvitationResource;
import org.exoplatform.calendar.ws.bean.RepeatResource;
import org.exoplatform.calendar.ws.bean.TaskResource;
import org.exoplatform.calendar.ws.common.Resource;
import org.exoplatform.calendar.ws.common.RestAPIConstants;
import org.exoplatform.common.http.HTTPStatus;
import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.commons.utils.MimeTypeResolver;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.rest.resource.ResourceContainer;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.security.Identity;
import org.exoplatform.webservice.cs.bean.End;
import org.exoplatform.ws.frameworks.json.impl.JsonGeneratorImpl;
import org.exoplatform.ws.frameworks.json.value.JsonValue;
import org.json.JSONException;
import org.json.JSONObject;

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;

/**
 * This rest service class provides entry point for calendar resources
 */
@Path(CalendarRestApi.CAL_BASE_URI)
public class CalendarRestApi implements ResourceContainer {

    public final static String CAL_BASE_URI = RestAPIConstants.BASE_VERSION_URI + "/calendar";

    public static final String TEXT_ICS = "text/calendar";
    public static final MediaType TEXT_ICS_TYPE = new MediaType("text", "calendar");

    // TODO: Why /cs/calendar is still being used here ?
    public final static String BASE_URL = "/cs/calendar";
    public final static String BASE_EVENT_URL = BASE_URL + "/event";

    public final static String CALENDAR_URI = "/calendars/";
    public final static String EVENT_URI = "/events/";
    public final static String TASK_URI = "/tasks/";
    public final static String ICS_URI = "/ics";
    public final static String ATTACHMENT_URI = "/attachments/";
    public final static String OCCURRENCE_URI = "/occurrences";
    public final static String CATEGORY_URI = "/categories/";
    public final static String FEED_URI = "/feeds/";
    public final static String RSS_URI = "/rss";
    public final static String INVITATION_URI = "/invitations/";
    public static final String HEADER_LINK = "Link";
    public static final String HEADER_LOCATION = "Location";

    private OrganizationService orgService;
    private int query_limit = 10;
    private SubResourceHrefBuilder subResourcesBuilder = new SubResourceHrefBuilder(this);

    private final static CacheControl nc = new CacheControl();

    public static final String DEFAULT_CAL_NAME = "calendar";

    public static final String DEFAULT_EVENT_NAME = "default";

    public static final String[] RP_WEEKLY_BYDAY = CalendarEvent.RP_WEEKLY_BYDAY.clone();

    public static final String[] EVENT_AVAILABILITY = { CalendarEvent.ST_AVAILABLE, CalendarEvent.ST_BUSY,
            CalendarEvent.ST_OUTSIDE };

    public static final String[] REPEATTYPES = CalendarEvent.REPEATTYPES.clone();

    public static final String[] PRIORITY = CalendarEvent.PRIORITY.clone();

    public static final String[] TASK_STATUS = CalendarEvent.TASK_STATUS.clone();

    private static final String[] INVITATION_STATUS = { "", "maybe", "yes", "no" };

    static {
        Arrays.sort(RP_WEEKLY_BYDAY);
        Arrays.sort(EVENT_AVAILABILITY);
        Arrays.sort(REPEATTYPES);
        Arrays.sort(PRIORITY);
        Arrays.sort(TASK_STATUS);
        Arrays.sort(INVITATION_STATUS);
    }

    private final CacheControl cc = new CacheControl();

    static {
        nc.setNoCache(true);
        nc.setNoStore(true);
    }

    private Log log = ExoLogger.getExoLogger(CalendarRestApi.class);

    /**
     * Contructor help to configure the rest service with parameters.
     *
     * Here is the configuration parameters:
     * - query_limit        maximum objects returned for a collection query, default value: 10.
     * - cache_maxage  time in miliseconds return in the cache-control header, default value:  604800
     *
     * @param  orgService
     *         exo organization service implementation
     *
     * @param  params
     *         Object contains the configuration parameters.
     */
    public CalendarRestApi(OrganizationService orgService, InitParams params) {
        this.orgService = orgService;

        int maxAge = 604800;
        if (params != null) {
            if (params.getValueParam("query_limit") != null) {
                query_limit = Integer.parseInt(params.getValueParam("query_limit").getValue());
            }

            ValueParam cacheConfig = params.getValueParam("cache_maxage");
            if (cacheConfig != null) {
                try {
                    maxAge = Integer.parseInt(cacheConfig.getValue());
                } catch (Exception ex) {
                    log.warn("Can't parse {} to maxAge, use the defalt value {}", cacheConfig, maxAge);
                }
            }
        }
        cc.setPrivate(true);
        cc.setMaxAge(maxAge);
        cc.setSMaxAge(maxAge);
    }

    /**
     * Return all the available subresources as json, in order to navigate easily in the REST API.
     *
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar
     *
     * @format  json
     *
     * @response
     *    {
     *        "subResourcesHref": [
     *            "http://localhost:8080/rest/calendar/calendars",
     *            "http://localhost:8080/rest/calendar/events",
     *            "http://localhost:8080/rest/calendar/tasks"
     *         ]
     *     }
     *
     * @return  All hrefs of available entry-point of calendar service
     *
     * @authentication
     *
     * @anchor  CalendarRestApi.getSubResources
     */
    @GET
    @RolesAllowed("users")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getSubResources(@Context UriInfo uri) {
        Map<String, String[]> subResources = new HashMap<String, String[]>();
        subResources.put("subResourcesHref", subResourcesBuilder.buildResourceMap(uri));

        return Response.ok(subResources, MediaType.APPLICATION_JSON).cacheControl(nc).build();
    }

    /**
     * Search for calendars which:
     * - the authenticated user is the owner of the calendar.
     * - the authenticated user belongs to the group of the calendar.
     * - the calendar has been shared with the authenticated user or with a group of the authenticated user.
     *
     * @param  type
     *         The calendar type to search for. It can be one of *personal, group, shared*.
     *         If this is omitted OR an unknown type is specified, it will search for *all* types.
     *
     * @param  offset
     *         The starting point when paging through a list of entities. Defaults to *0*.
     *
     * @param  limit
     *         The maximum number of results when paging through a list of entities, if not specify or exceed
     *         the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *         (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     *
     * @param  resturnSize
     *         Tell the service if it must return the total size of the returned collection result, 
     *         and the *link* http headers. 
     *         It can be true or false, by default, it's *false* 
     *
     * @param  fields
     *         This is a list of comma-separated property's names of response json object,
     *         if not specified, it return the json will all available properties.
     *
     * @param  jsonp
     *         The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *         json object is returned. 
     *
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/calendars?type=personal&fields=id,name
     *
     * @format  JSON
     *
     * @response 
     * [
     *   {
     *      id: "defaultId",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/calendars/defaultId",
     *      name: "calName",
     *      description: "...",
     *      type: "personal",
     *      timezone: "...",
     *      color: "...",
     *      owner: "...",
     *      viewPermission: "",
     *      editPermission: "",
     *      group: ["", ""],
     *      publicURL: "",
     *      privateURL: "",
     *      icsURL: ""
     *   }, 
     *   {id...}
     * ]
     * @return  List of calendars
     *
     * @authentication
     *
     * @anchor  CalendarRestApi.getCalendars
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @GET
    @RolesAllowed("users")
    @Path("/calendars/")
    @Produces({ MediaType.APPLICATION_JSON })
    public Response getCalendars(@QueryParam("type") String type, @QueryParam("offset") int offset,
            @QueryParam("limit") int limit, @QueryParam("returnSize") boolean returnSize,
            @QueryParam("fields") String fields, @QueryParam("jsonp") String jsonp, @Context UriInfo uri) {
        try {
            limit = parseLimit(limit);
            Type calType = Calendar.Type.UNDEFINED;

            if (type != null) {
                try {
                    calType = Calendar.Type.valueOf(type.toUpperCase());
                } catch (IllegalArgumentException ex) {
                    // Use default Type.UNDEFINED in any case of exception
                }
            }

            CalendarCollection<Calendar> cals = calendarServiceInstance().getAllCalendars(currentUserId(),
                    calType.type(), offset, limit);
            if (cals == null || cals.isEmpty())
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            String basePath = getBasePath(uri);
            Collection data = new LinkedList();
            Iterator<Calendar> calIterator = cals.iterator();
            while (calIterator.hasNext()) {
                Calendar cal = calIterator.next();
                data.add(extractObject(new CalendarResource(cal, basePath), fields));
            }

            CollectionResource calData = new CollectionResource(data, returnSize ? cals.getFullSize() : -1);
            calData.setOffset(offset);
            calData.setLimit(limit);

            ResponseBuilder okResult;
            if (jsonp != null) {
                JsonValue value = new JsonGeneratorImpl().createJsonObject(calData);
                StringBuilder sb = new StringBuilder(jsonp);
                sb.append("(").append(value).append(");");
                okResult = Response.ok(sb.toString(), new MediaType("text", "javascript"));
            } else {
                okResult = Response.ok(calData, MediaType.APPLICATION_JSON);
            }

            if (returnSize) {
                okResult.header(HEADER_LINK, buildFullUrl(uri, offset, limit, calData.getSize()));
            }

            //
            return okResult.cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();

    }

    /**
     * Creates a calendar if:
     * - this is a personal calendar and the user is authenticated.
     * - this is a group calendar and the user is authenticated and belongs to the group.
     * 
     * This entry point only allow http POST request, with json object (cal) in the request body. Example:
     *    {
     *      name: "Calendar name",
     *      description: "Description of the calendar",
     *      timezone: "...",
     *      color: "...",
     *      owner: "...",
     *      viewPermission: "...",
     *      editPermission: "...",
     *      group: ["", ""],
     *      publicURL: "", privateURL: ""
     *   }
     * 
     * @param  cal
     *         JSON object contains attributes of calendar object to create.
     *         All attributes are optional. If specified explicitly, calendar name must not empty, 
     *         contains only letter, digit, space, "-", "_" characters. Default value of calendar name is: calendar.
     *
     * @request  POST: http://localhost:8080/portal/rest/v1/calendar/calendars
     *
     * @response  HTTP status code: 
     *            201 if created successfully, and http header *location* href point to the newly created calendar.
     *            401 if user don't have create permission, 503 if there is any error during the save process
     *
     * @return  http status code
     *
     * @authentication
     *
     * @anchor  CalendarRestApi.createCalendar
     */
    @POST
    @RolesAllowed("users")
    @Path("/calendars/")
    public Response createCalendar(CalendarResource cal, @Context UriInfo uriInfo) {
        Calendar calendar = new Calendar();
        if (cal.getName() == null) {
            cal.setName(DEFAULT_CAL_NAME);
        }
        Response error = buildCalendar(calendar, cal);
        if (error != null) {
            return error;
        }

        if (cal.getGroups() != null && cal.getGroups().length > 0) {
            // Create a group calendar
            if (isInGroups(cal.getGroups())) {
                calendarServiceInstance().savePublicCalendar(calendar, true);
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } else {
            if (cal.getOwner() != null && !cal.getOwner().equals(currentUserId())) {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            } else {
                // Create a personal calendar
                calendarServiceInstance().saveUserCalendar(currentUserId(), calendar, true);
            }
        }

        return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, uriInfo.getAbsolutePath() + cal.getId())
                .cacheControl(nc).build();
    }

    /**
     * Returns the calendar with the specified id parameter if:
     * - The authenticated user is the owner of the calendar
     * - The authenticated user belongs to the group of the calendar
     * - The calendar has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param  id
     *         identity of the calendar to retrieve
     * 
     * @param  fields
     *         This is a list of comma-separated property's names of response json object,
     *         if not specified, it return the json will all available properties (id, href, name, description, type, timezone, 
     *         color, owner, viewPermission, editPermision, groups, publicURL, privateURL, icsURL)
     * 
     * @param  jsonp
     *         The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *         json object is returned 
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/calendars/{id}
     *
     * @format  JSON
     *
     * @response
     *  {
     *      id: "demo-defaultCalendarId",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId",
     *      name: "calName",
     *      description: "...",
     *      type: "personal",
     *      timezone: "...",
     *      color: "...",
     *      owner: "...",
     *      viewPermission: "",
     *      editPermission: "",
     *      group: ["", ""],
     *      publicURL: "",
     *      privateURL: "",
     *      icsURL: ""
     *   }
     * @return  calendar as JSON object
     *
     * @authentication
     * 
     * @anchor  CalendarRestApi.getCalendarById
     */
    @GET
    @RolesAllowed("users")
    @Path("/calendars/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getCalendarById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo, @Context Request request) {
        try {
            CalendarService service = calendarServiceInstance();
            Calendar cal = service.getCalendarById(id);
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            cal.setCalType(service.getTypeOfCalendar(currentUserId(), cal.getId()));

            Date lastModified = new Date(cal.getLastModified());
            ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
            if (preCondition != null) {
                return preCondition.build();
            }

            CalendarResource calData = null;
            if (this.hasViewCalendarPermission(cal, currentUserId())) {
                calData = new CalendarResource(cal, getBasePath(uriInfo));
            }
            if (calData == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Object resource = extractObject(calData, fields);
            if (jsonp != null) {
                String json = null;
                if (resource instanceof Map)
                    json = new JSONObject(resource).toString();
                else {
                    JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
                    json = generatorImpl.createJsonObject(resource).toString();
                }
                StringBuilder sb = new StringBuilder(jsonp);
                sb.append("(").append(json).append(");");
                return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(cc)
                        .lastModified(lastModified).build();
            }

            //
            return Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(cc).lastModified(lastModified)
                    .build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Update the calendar with specified id if:
     * - the authenticated user is the owner of the calendar
     * - for group calendars, the authenticated user has edit rights on the calendar
     * 
     * This entry point only allow http PUT request, with json object (calObj) in the request body, and calendar id in the path.
     * All the attributes of json object are optional, any omited attributes, or non-exists one will be ignored. *id* and *href* can't
     * be updated, they also be ignored.
     * For example:
     * {
     *      name: "calName",
     *      description: "...",
     *      type: "personal",
     *      timezone: "...",
     *      color: "...",
     *      owner: "...",
     *      viewPermission: "",
     *      editPermission: "",
     *      group: ["", ""],
     *      publicURL: "",
     *      privateURL: ""
     *   }
     *  
     * @param id
     *        identity of the calendar to update
     * 
     * @param calObj  
     *        json object contains attributes of calendar object to update, all the attributes are optional. 
     *        The calendar name must not empty, contains only letter, digit, space, "-", "_" characters
     * 
     * @request  PUT: http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId
     * 
     * @response  HTTP status code: 200 if updated successfully, 404 if calendar with provided id doesnt exists,
     *            401 if user don't have create permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.updateCalendarById
     */
    @PUT
    @RolesAllowed("users")
    @Path("/calendars/{id}")
    public Response updateCalendarById(@PathParam("id") String id, CalendarResource calObj) {
        try {
            Calendar cal = calendarServiceInstance().getCalendarById(id);
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            //Only allow to edit if user is owner of calendar, or have edit permission on group calendar
            //don't allow to edit shared calendar, or remote calendar
            if ((currentUserId().equals(cal.getCalendarOwner()) || cal.getGroups() != null)
                    && Utils.isCalendarEditable(currentUserId(), cal)) {
                Response error = buildCalendar(cal, calObj);
                if (error != null) {
                    return error;
                } else {
                    int type = calendarServiceInstance().getTypeOfCalendar(currentUserId(), cal.getId());
                    calendarServiceInstance().saveCalendar(cal.getCalendarOwner(), cal, type, false);
                    return Response.ok().cacheControl(nc).build();
                }
            }

            //
            return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
        } catch (Exception e) {
            log.error(e);
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Delete the calendar with the specified id if:
     * - the authenticated user is the owner of the calendar.
     * - for group calendars, the authenticated user has edit rights on the calendar.
     * - If it is a shared calendar the calendar is not shared anymore (but the original calendar is not deleted).
     * 
     * @param  id  
     *         identity of the calendar to delete
     * 
     * @request  DELETE: http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId
     * 
     * @response  HTTP status code: 200 if updated successfully, 404 if calendar with provided id doesnt exists,
     *            401 if user don't have create permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.deleteCalendarById
     */
    @DELETE
    @RolesAllowed("users")
    @Path("/calendars/{id}")
    public Response deleteCalendarById(@PathParam("id") String id) {
        try {
            Calendar cal = calendarServiceInstance().getCalendarById(id);
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            cal.setCalType(calendarServiceInstance().getTypeOfCalendar(currentUserId(), id));
            if (Utils.isCalendarEditable(currentUserId(), cal) || cal.getCalType() == Calendar.TYPE_SHARED) {
                switch (cal.getCalType()) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().removeUserCalendar(cal.getCalendarOwner(), id);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().removePublicCalendar(id);
                    break;
                case Calendar.TYPE_SHARED:
                    if (this.hasViewCalendarPermission(cal, currentUserId())) {
                        calendarServiceInstance().removeSharedCalendar(currentUserId(), id);
                        break;
                    }
                }
                return Response.ok().cacheControl(nc).build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns an iCalendar formated file which is exported from the calendar with specified id if:
     * - the calendar is public
     * - the authenticated user is the owner of the calendar
     * - the authenticated user belongs to the group of the calendar
     * - the calendar has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id   
     *        identity of the calendar to retrieve ICS file
     * 
     * @request GET: http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId/ics
     * 
     * @format text/calendar
     * 
     * @response ICS file of calendar, or HTTP status code: 404 if not found
     * 
     * @return  ICS file or Http status code
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.exportCalendarToIcs
     */
    @GET
    @RolesAllowed("users")
    @Path("/calendars/{id}/ics")
    @Produces(TEXT_ICS)
    public Response exportCalendarToIcs(@PathParam("id") String id, @Context Request request) {
        try {
            Calendar cal = calendarServiceInstance().getCalendarById(id);
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())) {
                int type = calendarServiceInstance().getTypeOfCalendar(currentUserId(), id);
                String username = currentUserId();
                if (type == -1) {
                    //this is a workaround
                    //calendarService can't find type of calendar correctly 
                    type = Calendar.TYPE_PRIVATE;
                    username = cal.getCalendarOwner();
                }

                CalendarImportExport iCalExport = calendarServiceInstance()
                        .getCalendarImportExports(CalendarService.ICALENDAR);
                ArrayList<String> calIds = new ArrayList<String>();
                calIds.add(id);
                OutputStream out = iCalExport.exportCalendar(username, calIds, String.valueOf(type),
                        Utils.UNLIMITED);
                byte[] data = out.toString().getBytes();

                byte[] hashCode = digest(data).getBytes();
                EntityTag tag = new EntityTag(new String(hashCode));
                ResponseBuilder preCondition = request.evaluatePreconditions(tag);
                if (preCondition != null) {
                    return preCondition.build();
                }

                InputStream in = new ByteArrayInputStream(data);
                return Response.ok(in, TEXT_ICS_TYPE)
                        .header("Content-Disposition", "attachment;filename=\"" + cal.getName() + Utils.ICS_EXT)
                        .cacheControl(cc).tag(tag).build();
            } else {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();

    }

    /**
     * Returns an event with specified id parameter if:
     * - the calendar of the event is public
     * - the authenticated user is the owner of the calendar of the event
     * - the authenticated user belongs to the group of the calendar of the event
     * - the authenticated user is a participant of the event
     * - the calendar of the event has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id              
     *        identity of event to find
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=calendar,categories. In case of collections, 
     *        you can put offset (default: 0), limit (default: *query_limit*) value into param. For example, expand=categories(1,5).
     *        Instead of: 
     *        {
     *            id: "...", 
     *            calendar: "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            calendar: {
     *            id: "...",
     *            name:"demo-"efaultId",
     *            ....
     *        }
     *            ....
     *        }
     * 
     * @request GET: http://localhost:8080/portal/rest/v1/calendar/events/Event123
     * 
     * @format JSON
     * 
     * @response 
     * {
     *      id: "defaultId",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/events/Event123",
     *      subject: "..",
     *      description: "...",
     *      from: "...",
     *      to: "...",
     *      calendar: "...",
     *      categories: ["...", ""],
     *      location: "",
     *      priority: "", 
     *      repeat: {...},
     *      recurrenceId: "...",
     *      originalEvent: "...",
     *      reminder: [],
     *      attachment: [],
     *      participants: [],
     *      privacy: "",
     *      availability: "" 
     * }
     * 
     * @return        event as json object
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getEventById
     */
    @GET
    @RolesAllowed("users")
    @Path("/events/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getEventById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("expand") String expand, @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo,
            @Context Request request) {
        try {
            CalendarService service = calendarServiceInstance();
            CalendarEvent ev = service.getEventById(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Date lastModified = new Date(ev.getLastModified());
            ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
            if (preCondition != null) {
                return preCondition.build();
            }

            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
            boolean inParticipant = false;
            String[] participant = ev.getParticipant();
            if (participant != null) {
                Arrays.sort(participant);
                if (Arrays.binarySearch(participant, currentUserId()) > -1)
                    inParticipant = true;
            }

            if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())
                    || inParticipant) {
                Object resource = buildEventResource(ev, uriInfo, expand, fields);
                return buildJsonP(resource, jsonp).cacheControl(cc).lastModified(lastModified).build();
            } else {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Updates the event with specified id if:
     * - the authenticated user is the owner of the calendar of the event
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar of the event has been shared with the authenticated user, with modification rights
     * - the calendar of the event has been shared with a group of the authenticated user, with modification rights
     * 
     * This entry point only allow http PUT request, with json object (evObject) in the request body, and event id in the path.
     * All the attributes of json object are optional, any omited attributes, or non-exists one will be ignored. 
     * Or read-only attributes: *id* and *href*, *originalEvent*, *calendar*, *recurrentId* can't be updated, they also be ignored.
     * For example:
     * {
     *      subject: "..",
     *      description: "...",
     *      categoryId: "",
     *      from: "...", to: "...",
     *      location: "",
     *      priority: "", 
     *      repeat: {...},
     *      reminder: [],
     *      privacy: "",
     *      availability: ""
     * }
     *  
     * @param id             
     *        identity of the event to update
     * 
     * @param evObject  
     *        json object contains attributes of event object to update, all the attributes are optional.
     *        If provided explitly (not null), attributes are checked with some rules:
     *        1. subject must not be empty
     *        2. availability can only be one of "available", "busy", "outside"
     *        3. repeat.repeatOn can only be one of"MO", "TU", "WE", "TH", "FR", "SA", "SU"
     *        4. repeat.repeatBy must be >= 1 and <= 31
     *        5. repeat.repeatType must be one of "norepeat", "daily", "weekly", "monthly", "yearly"
     *        6. "from" date must be before "to" date
     *        7. priority must be one of "none", "high", "normal", "low"
     *        8. privacy can only be public or private
     * 
     * @request PUT: http://localhost:8080/portal/rest/v1/calendar/events/Event123
     * 
     * @response  HTTP status code: 200 if updated successfully, 400 if parameters are not valid 
     *            404 if event with provided id doesnt exists,
     *            401 if user don't have create permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.updateEventById
     */
    @PUT
    @RolesAllowed("users")
    @Path("/events/{id}")
    public Response updateEventById(@PathParam("id") String id, EventResource evObject) {
        try {
            CalendarEvent old = calendarServiceInstance().getEventById(id);
            if (old == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Calendar cal = calendarServiceInstance().getCalendarById(old.getCalendarId());
            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                Response error = buildEvent(old, evObject);
                if (error != null) {
                    return error;
                }

                int calType = -1;
                try {
                    calType = Integer.parseInt(old.getCalType());
                } catch (NumberFormatException e) {
                    calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), old.getCalendarId());
                }
                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().saveUserEvent(currentUserId(), old.getCalendarId(), old, false);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().savePublicEvent(old.getCalendarId(), old, false);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().saveEventToSharedCalendar(currentUserId(), old.getCalendarId(), old,
                            false);
                    break;

                default:
                    break;
                }
                return Response.ok().cacheControl(nc).build();
            }

            //
            return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Delete an event with specified id parameter if:
     * - the authenticated user is the owner of the calendar of the event
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar of the event has been shared with the authenticated user, with modification rights
     * - the calendar of the event has been shared with a group of the authenticated user, with modification rights
     * 
     * @param id  
     *        identity of the event to delete
     * 
     * @request DELETE: http://localhost:8080/portal/rest/v1/calendar/events/Event123
     * 
     * @response  HTTP status code: 200 if delete successfully, 404 if event with provided id doesnt exists,
     *            401 if user don't have permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.deleteCalendarById
     */
    @DELETE
    @RolesAllowed("users")
    @Path("/events/{id}")
    public Response deleteEventById(@PathParam("id") String id) {
        try {
            CalendarEvent ev = calendarServiceInstance().getEventById(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                int calType = Calendar.TYPE_ALL;
                try {
                    calType = Integer.parseInt(ev.getCalType());
                } catch (NumberFormatException e) {
                    calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), ev.getCalendarId());
                }
                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().removeUserEvent(currentUserId(), ev.getCalendarId(), id);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().removePublicEvent(ev.getCalendarId(), id);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().removeSharedEvent(currentUserId(), ev.getCalendarId(), id);
                    break;

                default:
                    break;
                }
                return Response.ok().cacheControl(nc).build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns attachments of an event with specified id if:
     * - the calendar of the event is public
     * - the authenticated user is the owner of the calendar of the event
     * - the authenticated user belongs to the group of the calendar of the event
     * - the authenticated user is a participant of the event
     * - the calendar of the event has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id        
     *        identity of event that being query for attachments
     *
     * @param offset  
     *        The starting point when paging through a list of entities. Defaults to *0*.
     * 
     * @param limit   
     *        The maximum number of results when paging through a list of entities, if not specify or exceed
     *        the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *        (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     * 
     * @param fields  
     *        This is a list of comma-separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp   
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned. 
     * 
     * @request GET: http://localhost:8080/portal/rest/v1/calendar/events/Event123/attachments
     * 
     * @format JSON
     * 
     * @response 
     *  [
     *    {
     *        id: "...",
     *        href: "...",
     *        name: "...",
     *        mimeType: "...",
     *        weight: "..."
     *    }, {...}
     *  ]
     *  
     * @return        List of attachments as json array
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getAttachmentsFromEvent
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @GET
    @RolesAllowed("users")
    @Path("/events/{id}/attachments")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getAttachmentsFromEvent(@PathParam("id") String id, @QueryParam("offset") int offset,
            @QueryParam("limit") int limit, @QueryParam("fields") String fields, @QueryParam("jsonp") String jsonp,
            @Context UriInfo uriInfo) {
        try {
            limit = parseLimit(limit);

            CalendarEvent ev = calendarServiceInstance().getEventById(id);
            if (ev == null || ev.getAttachment() == null) {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            } else {
                Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
                boolean inParticipant = false;
                if (ev.getParticipant() != null) {
                    String[] participant = ev.getParticipant();
                    Arrays.sort(participant);
                    int i = Arrays.binarySearch(participant, currentUserId());
                    if (i > -1)
                        inParticipant = true;
                }

                if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())
                        || inParticipant) {
                    Iterator<Attachment> it = ev.getAttachment().iterator();
                    List attResource = new ArrayList();
                    Utils.skip(it, offset);
                    int counter = 0;
                    String basePath = getBasePath(uriInfo);
                    while (it.hasNext()) {
                        Attachment a = it.next();
                        attResource.add(extractObject(new AttachmentResource(a, basePath), fields));
                        if (++counter == limit)
                            break;
                    }
                    CollectionResource evData = new CollectionResource(attResource, ev.getAttachment().size());
                    evData.setOffset(offset);
                    evData.setLimit(limit);

                    if (jsonp != null) {
                        JsonValue value = new JsonGeneratorImpl().createJsonObject(evData);
                        StringBuilder sb = new StringBuilder(jsonp);
                        sb.append("(").append(value).append(");");
                        return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(nc)
                                .header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, evData.getSize()))
                                .build();
                    }

                    //
                    return Response.ok(evData, MediaType.APPLICATION_JSON)
                            .header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, evData.getSize()))
                            .cacheControl(nc).build();
                }

                //
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Creates attachments for an event with specified id if:
     * - the authenticated user is the owner of the calendar of the event
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar of the event has been shared with the authenticated user, with modification rights
     * - the calendar of the event has been shared with a group of the authenticated user, with modification rights
     * 
     * This entry point only allow http POST request, with file input stream in the http form submit, and the id of event in the path
     * 
     * @param id      
     *        identity of event to create attachment 
     *
     * @param iter   
     *        Iterator of org.apache.commons.fileupload.FileItem object 
     *        (eXo rest framework use apache file upload to parse the input stream of http form submit request, and inject FileItem objects
     *
     * @request POST: http://localhost:8080/portal/rest/v1/calendar/events/Event123/attachments
     * 
     * @response  HTTP status code:
     *            201 if created successfully, and http header *location* href point to the newly created attachment resource.
     *            404 if event to create attachment not found 
     *            401 if user don"t have create permission, 503 if there is any error during the save process
     * 
     * @return http status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.createAttachmentForEvent
     */
    @POST
    @RolesAllowed("users")
    @Path("/events/{id}/attachments")
    @Consumes("multipart/*")
    public Response createAttachmentForEvent(@Context UriInfo uriInfo, @PathParam("id") String id,
            Iterator<FileItem> iter) {
        try {
            CalendarEvent ev = calendarServiceInstance().getEventById(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());

            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                int calType = Calendar.TYPE_ALL;
                List<Attachment> attachment = new ArrayList<Attachment>();
                try {
                    calType = Integer.parseInt(ev.getCalType());
                } catch (NumberFormatException e) {
                    calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), ev.getCalendarId());
                }

                attachment.addAll(ev.getAttachment());
                while (iter.hasNext()) {
                    FileItem file = iter.next();
                    String fileName = file.getName();
                    if (fileName != null) {
                        String mimeType = new MimeTypeResolver().getMimeType(fileName.toLowerCase());
                        Attachment at = new Attachment();
                        at.setMimeType(mimeType);
                        at.setSize(file.getSize());
                        at.setName(file.getName());
                        at.setInputStream(file.getInputStream());
                        attachment.add(at);
                    }
                }
                ev.setAttachment(attachment);

                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().saveUserEvent(currentUserId(), ev.getCalendarId(), ev, false);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().savePublicEvent(ev.getCalendarId(), ev, false);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().saveEventToSharedCalendar(currentUserId(), ev.getCalendarId(), ev,
                            false);
                    break;
                default:
                    break;
                }

                StringBuilder attUri = new StringBuilder(getBasePath(uriInfo));
                attUri.append("/").append(ev.getId());
                attUri.append(ATTACHMENT_URI);
                return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, attUri.toString())
                        .cacheControl(nc).build();
            }

            //
            return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns events of an calendar with specified id when:
     * - the calendar is public
     * - the authenticated user is the owner of the calendar of the event
     * - the authenticated user belongs to the group of the calendar of the event
     * - the authenticated user is a participant of the event
     * - the calendar of the event has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id              
     *        identity of a calendar to search for events
     * 
     * @param start         
     *        date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.
     *        Default: current server time.
     * 
     * @param end           
     *        date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date
     *        Default: current server time + 1 week.
     * 
     * @param category  
     *        search for this category only. If not specify, search event of any category
     *
     * @param offset       
     *        The starting point when paging through a list of entities. Defaults to *0*.
     * 
     * @param limit         
     *        The maximum number of results when paging through a list of entities, if not specify or exceed
     *        the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *        (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     * 
     * @param resturnSize  
     *        tell the service if it must return the total size of the returned collection result, and the *link* http headers. 
     *        It can be true or false, by default, it's *false*
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=calendar,categories. In case of collections, 
     *        you can put offset (default: 0), limit (default: *query_limit* of the rest service) value into param, for example: expand=categories(1,5).
     *        Instead of: 
     *        {
     *            id: "...", 
     *            calendar: "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            calendar: {
     *            id: "...",
     *            name:"demo-defaultId",
     *            ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/myCalId/events?category=meeting&expand=calendar,categories(1,5)
     * 
     * @format JSON
     * 
     * @response 
     * [
     *   {
     *      id: "myEventId",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/events/myEventId",
     *      subject: "..", description: "...",
     *      from: "...", to: "...",
     *      calendar: "...", categories: ["...", "..."],
     *      location: "", priority: "", 
     *      repeat: {...},
     *      recurrenceId: "...", originalEvent: "...",
     *      reminder: [], attachment: [], participants: [],
     *      privacy: "", availability: "" 
     *   }, 
     *   {id...}
     * ]
     * 
     * @return  List of events of a specific calendar
     * 
     * @authentication
     *     
     * @anchor CalendarRestApi.getEventsByCalendar
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @GET
    @RolesAllowed("users")
    @Path("/calendars/{id}/events")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getEventsByCalendar(@PathParam("id") String id, @QueryParam("startTime") String start,
            @QueryParam("endTime") String end, @QueryParam("category") String category,
            @QueryParam("offset") int offset, @QueryParam("limit") int limit, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @QueryParam("expand") String expand,
            @QueryParam("returnSize") boolean returnSize, @Context UriInfo uri) throws Exception {
        limit = parseLimit(limit);
        String username = currentUserId();

        CalendarService service = calendarServiceInstance();
        EventDAO evtDAO = service.getEventDAO();

        long fullSize = returnSize ? 0 : -1;
        List data = new LinkedList();
        Calendar calendar = service.getCalendarById(id);

        if (calendar != null) {
            if (calendar.hasChildren()) {
                String participant = null;
                if (calendar.getPublicUrl() == null && !hasViewCalendarPermission(calendar, username)) {
                    participant = username;
                }

                EventQuery eventQuery = buildEventQuery(start, end, category, Arrays.asList(calendar), id,
                        participant, CalendarEvent.TYPE_EVENT);
                ListAccess<CalendarEvent> events = evtDAO.findEventsByQuery(eventQuery);

                //
                for (CalendarEvent event : events.load(offset, limit)) {
                    data.add(buildEventResource(event, uri, expand, fields));
                }
                if (returnSize) {
                    fullSize = events.getSize();
                }
            }
        } else {
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
        }
        //
        CollectionResource evData = new CollectionResource(data, fullSize);
        evData.setOffset(offset);
        evData.setLimit(limit);

        ResponseBuilder response = buildJsonP(evData, jsonp);

        if (returnSize) {
            response.header(HEADER_LINK, buildFullUrl(uri, offset, limit, fullSize));
        }

        //
        return response.build();
    }

    /**
     * Creates an event in a calendar with specified id only if:
     * - the authenticated user is the owner of the calendar
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar has been shared with the authenticated user, with modification rights
     * - the calendar has been shared with a group of the authenticated user, with modification rights
     * 
     * This entry point only allow http POST request, with json object (evObject) in the request body. Example:
     *    {
     *      categoryId: "",
     *      subject: "..", description: "...",
     *      from: "...", to: "...",
     *      location: "", priority: "", 
     *      repeat: {...},
     *      reminder: [],
     *      privacy: "", availability: "" 
     *   }
     * 
     * @param evObject    
     *        json object contains attributes of event object to create.
     *        All attribute are optional. If provided explitly (not null), attributes are checked with some rules:
     *        1. subject must not be empty, default value is: default
     *        2. availability can only be one of "available", "busy", "outside"
     *        3. repeat.repeatOn can only be one of"MO", "TU", "WE", "TH", "FR", "SA", "SU"
     *        4. repeat.repeatBy must be >= 1 and <= 31
     *        5. repeat.repeatType must be one of "norepeat", "daily", "weekly", "monthly", "yearly"
     *        6. "from" date must be before "to" date
     *        7. priority must be one of "none", "high", "normal", "low"
     *        8. privacy can only be public or private
     * 
     * @param id                
     *        identity of the *calendar* to create event
     * 
     * @request  POST: http://localhost:8080/portal/rest/v1/calendar/calendars/myCalId/events
     *
     * @response  HTTP status code: 
     *            201 if created successfully, and http header *location* href point to the newly created event.
     *            400 if provided attributes are not valid (not following the rule of evObject)
     *            404 if no calendar found with provided id.
     *            401 if user don't have create permission, 503 if there is any error during the save process.
     * 
     * @return  http status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.createEventForCalendar
     */
    @POST
    @RolesAllowed("users")
    @Path("/calendars/{id}/events")
    public Response createEventForCalendar(@PathParam("id") String id, EventResource evObject,
            @Context UriInfo uriInfo) {
        try {
            Calendar cal = calendarServiceInstance().getCalendarById(id);
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            CalendarEvent evt = new CalendarEvent();
            if (evObject.getSubject() == null) {
                evObject.setSubject(DEFAULT_EVENT_NAME);
            }
            if (evObject.getCategoryId() == null) {
                evObject.setCategoryId(NewUserListener.DEFAULT_EVENTCATEGORY_ID_ALL);
            }
            Response error = buildEvent(evt, evObject);
            if (error != null) {
                return error;
            }
            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), id);
                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().saveUserEvent(currentUserId(), id, evt, true);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().savePublicEvent(id, evt, true);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().saveEventToSharedCalendar(currentUserId(), id, evt, true);
                    break;
                default:
                    break;
                }

                String location = new StringBuilder(getBasePath(uriInfo)).append(EVENT_URI).append(evt.getId())
                        .toString();
                return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc)
                        .build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();

    }

    /**
     * Returns occurrences of a recurring event with specified id when :
     * the calendar of the event is public
     * the authenticated user is the owner of the calendar of the event
     * the authenticated user belongs to the group of the calendar of the event
     * the authenticated user is a participant of the event
     * the calendar of the event has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id             
     *        identity of recurrence event, if event not exists, return 404 http status code
     * 
     * @param start         
     *        date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.
     *        Default: current server time.
     * 
     * @param end           
     *        date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date
     *        Default: current server time + 1 week.
     *
     * @param offset       
     *        The starting point when paging through a list of entities. Defaults to *0*.
     * 
     * @param limit         
     *        The maximum number of results when paging through a list of entities, if not specify or exceed
     *        the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *        (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     * 
     * @param resturnSize  
     *        tell the service if it must return the total size of the returned collection result, and the *link* http headers. 
     *        It can be true or false, by default, it's *false*
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=calendar,categories. In case of collections, 
     *        you can put offset (default: 0), limit (default: *query_limit* of the rest service) value into param, for example: expand=categories(1,5).
     *        Instead of: 
     *        {
     *            id: "...", 
     *            calendar: "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            calendar: {
     *            id: "...",
     *            name:"demo-defaultId",
     *            ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/events/Event123/occurences?offset=1&limit=5
     * 
     * @format  JSON
     * 
     * @response 
     * [
     *   {
     *      id: "myEventId",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/events/myEventId",
     *      subject: "..", description: "...",
     *      from: "...", to: "...",
     *      calendar: "...", categories: ["...", ""],
     *      location: "", priority: "", 
     *      repeat: {...},
     *      recurrenceId: "...", originalEvent: "...",
     *      reminder: [], attachment: [], participants: [],
     *      privacy: "", availability: "" 
     *   }, 
     *   {id...}
     * ]
     * 
     * @return        List of occurrence events
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getOccurrencesFromEvent
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @GET
    @RolesAllowed("users")
    @Path("/events/{id}/occurrences")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getOccurrencesFromEvent(@PathParam("id") String id, @QueryParam("offset") int offset,
            @QueryParam("limit") int limit, @QueryParam("start") String start, @QueryParam("end") String end,
            @QueryParam("fields") String fields, @QueryParam("jsonp") String jsonp,
            @QueryParam("expand") String expand, @QueryParam("returnSize") boolean returnSize,
            @Context UriInfo uriInfo) {
        try {
            limit = parseLimit(limit);
            java.util.Calendar[] dates = parseDate(start, end);

            CalendarEvent recurEvent = calendarServiceInstance().getEventById(id);
            if (recurEvent == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            TimeZone tz = java.util.Calendar.getInstance().getTimeZone();
            String timeZone = tz.getID();

            Map<String, CalendarEvent> occMap = calendarServiceInstance().getOccurrenceEvents(recurEvent, dates[0],
                    dates[1], timeZone);
            if (occMap == null || occMap.isEmpty()) {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }

            Calendar cal = calendarServiceInstance().getCalendarById(recurEvent.getCalendarId());
            boolean inParticipant = false;
            if (recurEvent.getParticipant() != null) {
                String[] participant = recurEvent.getParticipant();
                Arrays.sort(participant);
                int i = Arrays.binarySearch(participant, currentUserId());
                if (i > -1)
                    inParticipant = true;
            }

            if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())
                    || inParticipant) {
                Collection data = new ArrayList();
                Iterator<CalendarEvent> evIter = occMap.values().iterator();
                Utils.skip(evIter, offset);

                int counter = 0;
                while (evIter.hasNext()) {
                    data.add(buildEventResource(evIter.next(), uriInfo, expand, fields));
                    if (++counter == limit)
                        break;
                }

                int fullSize = returnSize ? occMap.values().size() : -1;
                CollectionResource evData = new CollectionResource(data, fullSize);
                evData.setOffset(offset);
                evData.setOffset(limit);

                //
                ResponseBuilder response = buildJsonP(evData, jsonp);
                if (returnSize) {
                    response.header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, evData.getSize()));
                }
                return response.build();
            }

            //
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns tasks of a calendar with specified id when:
     * - the calendar is public
     * - the authenticated user is the owner of the calendar of the task
     * - the authenticated user belongs to the group of the calendar of the task
     * - the authenticated user is delegated by the task
     * - the calendar of the task has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id              
     *        identity of a calendar to search for tasks
     * 
     * @param start         
     *        date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.
     *        Default: current server time.
     * 
     * @param end           
     *        date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date
     *        Default: current server time + 1 week.
     * 
     * @param category  
     *        search for this category only. If not specify, search task of any category
     *
     * @param offset       
     *        The starting point when paging through a list of entities. Defaults to *0*.
     * 
     * @param limit         
     *        The maximum number of results when paging through a list of entities, if not specify or exceed
     *        the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *        (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     * 
     * @param resturnSize  
     *        tell the service if it must return the total size of the returned collection result, and the *link* http headers. 
     *        It can be true or false, by default, it's *false*
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=calendar,categories. In case of collections, 
     *        you can put offset (default: 0), limit (default: *query_limit* of the rest service) value into param, for example: expand=categories(1,5).
     *        Instead of: 
     *        {
     *            id: "...", 
     *            calendar: "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            calendar: {
     *            id: "...",
     *            name:"demo-defaultId",
     *            ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/myCalId/tasks?category=meeting&expand=calendar,categories(1,5)
     * 
     * @format JSON
     * 
     * @response 
     * [
     *   {
     *      id: "Task123",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/tasks/Task123",
     *      name: "..", note: "...",
     *      from: "...", to: "...",
     *      calendar: "...", categories: ["...", ""],
     *      delegation: ["...", ""], priority: "", 
     *      reminder: [], attachment: [],
     *      status: ""
     *   }, 
     *   {id...}
     * ]
     * 
     * @return  List of tasks of a specific calendar
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getTasksByCalendar
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @GET
    @RolesAllowed("users")
    @Path("/calendars/{id}/tasks")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getTasksByCalendar(@PathParam("id") String id, @QueryParam("startTime") String start,
            @QueryParam("endTime") String end, @QueryParam("category") String category,
            @QueryParam("offset") int offset, @QueryParam("limit") int limit, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @QueryParam("expand") String expand,
            @QueryParam("returnSize") boolean returnSize, @Context UriInfo uri) throws Exception {
        limit = parseLimit(limit);
        String username = currentUserId();

        CalendarService service = calendarServiceInstance();
        EventDAO evtDAO = service.getEventDAO();

        long fullSize = returnSize ? 0 : -1;
        List data = new LinkedList();
        Calendar calendar = service.getCalendarById(id);

        if (calendar != null) {
            String participant = null;
            if (calendar.getPublicUrl() == null && !hasViewCalendarPermission(calendar, username)) {
                participant = username;
            }

            EventQuery eventQuery = buildEventQuery(start, end, category, Arrays.asList(calendar), id, participant,
                    CalendarEvent.TYPE_TASK);
            ListAccess<CalendarEvent> events = evtDAO.findEventsByQuery(eventQuery);

            //
            for (CalendarEvent event : events.load(offset, limit)) {
                data.add(buildTaskResource(event, uri, expand, fields));
            }
            if (returnSize) {
                fullSize = events.getSize();
            }
        } else {
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
        }
        //
        CollectionResource evData = new CollectionResource(data, fullSize);
        evData.setOffset(offset);
        evData.setLimit(limit);

        ResponseBuilder response = buildJsonP(evData, jsonp);

        if (returnSize) {
            response.header(HEADER_LINK, buildFullUrl(uri, offset, limit, fullSize));
        }

        //
        return response.build();
    }

    /**
     * Creates a task for a calendar with specified id only if:
     * - the authenticated user is the owner of the calendar
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar has been shared with the authenticated user, with modification rights
     * - the calendar has been shared with a group of the authenticated user, with modification rights
     * 
     * This entry point only allow http POST request, with json object (evObject) in the request body. Example:
     *    {
     *      name: "..", note: "...",
     *      categoryId: "",
     *      from: "...", to: "...",
     *      delegation: ["...", ""], priority: "", 
     *      reminder: [],
     *      status: ""
     *   }
     * 
     * @param evObject    
     *        json object contains attributes of task object to create.
     *        All attribute are optional. If provided explitly (not null), attributes are checked with some rules:
     *        1. name must not be empty, default value is: "default".
     *        2. "from" date must be before "to" date
     *        3. priority must be one of "none", "high", "normal", "low"
     *        4. status must be one of "needs-action", "completed", "in-progress", "canceled"
     * 
     * @param id                
     *        identity of the *calendar* to create task
     * 
     * @request  POST: http://localhost:8080/portal/rest/v1/calendar/calendars/myCalId/tasks
     * 
     * @response  HTTP status code: 
     *            201 if created successfully, and http header *location* href point to the newly created task.
     *            400 if provided attributes are not valid (not following the rule of evObject)
     *            404 if no calendar found with provided id.
     *            401 if user don't have create permission, 503 if there is any error during the save process.
     * 
     * @return  http status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.createTaskForCalendar
     */
    @POST
    @RolesAllowed("users")
    @Path("/calendars/{id}/tasks")
    public Response createTaskForCalendar(@PathParam("id") String id, TaskResource evObject,
            @Context UriInfo uriInfo) {
        try {
            Calendar cal = calendarServiceInstance().getCalendarById(id);
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            CalendarEvent evt = new CalendarEvent();
            evt.setEventType(CalendarEvent.TYPE_TASK);
            if (evObject.getName() == null) {
                evObject.setName(DEFAULT_EVENT_NAME);
            }
            if (evObject.getCategoryId() == null) {
                evObject.setCategoryId(NewUserListener.DEFAULT_EVENTCATEGORY_ID_ALL);
            }
            Response error = buildEventFromTask(evt, evObject);
            if (error != null) {
                return error;
            }
            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), id);
                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().saveUserEvent(currentUserId(), id, evt, true);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().savePublicEvent(id, evt, true);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().saveEventToSharedCalendar(currentUserId(), id, evt, true);
                    break;
                default:
                    break;
                }

                String location = new StringBuilder(getBasePath(uriInfo)).append(TASK_URI).append(evt.getId())
                        .toString();
                return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc)
                        .build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();

    }

    /**
     *
     * Returns a task with specified id if: same rules as /events/{id}
     * {@link #getEventById(String, String, String, String)}
     * 
     * @param id              
     *        identity of task to find
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=calendar,categories. In case of collections, 
     *        you can put offset (default: 0), limit (default: *query_limit* of the rest service) value into param, for example: expand=categories(1,5).
     *        Instead of: 
     *        {
     *            id: "...", 
     *            calendar: "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            calendar: {
     *            id: "...",
     *            name:"demo-defaultId",
     *            ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/tasks/Task123?fields=id,name
     * 
     * @format JSON
     * 
     * @response 
     *   {
     *      id: "Task123",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/tasks/Task123",
     *      name: "..", note: "...",
     *      from: "...", to: "...",
     *      calendar: "...", categories: ["...", ""],
     *      delegation: ["...", ""], priority: "", 
     *      reminder: [], attachment: [],
     *      status: ""
     *   } 
     *  
     * @return  return task as json object
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getTaskById
     */
    @GET
    @RolesAllowed("users")
    @Path("/tasks/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getTaskById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("expand") String expand, @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo,
            @Context Request request) {
        try {
            CalendarEvent ev = calendarServiceInstance().getEventById(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Date lastModified = new Date(ev.getLastModified());
            ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
            if (preCondition != null) {
                return preCondition.build();
            }

            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
            boolean inParticipant = false;
            if (ev.getParticipant() != null) {
                String[] participant = ev.getParticipant();
                Arrays.sort(participant);
                if (Arrays.binarySearch(participant, currentUserId()) > -1)
                    inParticipant = true;
                ;
            }

            if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())
                    || inParticipant) {
                Object resource = buildTaskResource(ev, uriInfo, expand, fields);
                return buildJsonP(resource, jsonp).cacheControl(cc).lastModified(lastModified).build();
            } else {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     *
     * Updates a task with the specified id if:
     * - the authenticated user is the owner of the calendar of the event
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar of the event has been shared with the authenticated user, with modification rights
     * - the calendar of the event has been shared with a group of the authenticated user, with modification rights
     * 
     * This entry point only allow http PUT request, with json object (evObject) in the request body, and task id in the path.
     * All the attributes of json object are optional, any omited attributes, or non-exists one will be ignored. *id* and *href*, *calendar* can't
     * be updated, they also be ignored.
     * For example:
     *   {
     *      name: "..", note: "...",
     *      categoryId: "...",
     *      from: "...", to: "...",
     *      delegation: ["...", ""], priority: "", 
     *      reminder: [],
     *      status: ""
     *   } 
     *  
     * @param id             
     *        identity of the task to update
     * 
     * @param evObject  
     *        json object contains attributes of task object to update, all the attributes are optional.
     *        If provided explitly (not null), attributes are checked with some rules:
     *        1. name must not be empty
     *        2. "from" date must be before "to" date
     *        3. priority must be one of "none", "high", "normal", "low"
     *        4. status must be one of "needs-action", "completed", "in-progress", "canceled" 
     * 
     * @request  PUT: http://localhost:8080/portal/rest/v1/calendar/tasks/Task123
     * 
     * @response  HTTP status code:
     *            200 if updated successfully, 404 if task with provided id doesnt exists,
     *            400 if provided attributes are not valid, 
     *            401 if user don't have create permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.updateTaskById
     */
    @PUT
    @RolesAllowed("users")
    @Path("/tasks/{id}")
    public Response updateTaskById(@PathParam("id") String id, TaskResource evObject) {
        try {
            CalendarEvent old = calendarServiceInstance().getEventById(id);
            if (old == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            Calendar cal = calendarServiceInstance().getCalendarById(old.getCalendarId());
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                int calType = -1;
                try {
                    calType = Integer.parseInt(old.getCalType());
                } catch (NumberFormatException e) {
                    calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), old.getCalendarId());
                }
                buildEventFromTask(old, evObject);
                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().saveUserEvent(currentUserId(), old.getCalendarId(), old, false);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().savePublicEvent(old.getCalendarId(), old, false);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().saveEventToSharedCalendar(currentUserId(), old.getCalendarId(), old,
                            false);
                    break;

                default:
                    break;
                }
                return Response.ok().cacheControl(nc).build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Deletes a task with specified id if:
     * - the authenticated user is the owner of the calendar of the event
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar of the event has been shared with the authenticated user, with modification rights
     * - the calendar of the event has been shared with a group of the authenticated user, with modification rights
     *  
     * @param id  
     *        identity of the task to delete
     * 
     * @request  DELETE: http://localhost:8080/portal/rest/v1/calendar/tasks/Task123
     * 
     * @response  HTTP status code:
     *            200 if delete successfully, 404 if task with provided id doesnt exists,
     *            401 if user don't have permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.deleteTaskById
     */
    @DELETE
    @RolesAllowed("users")
    @Path("/tasks/{id}")
    public Response deleteTaskById(@PathParam("id") String id) {
        try {
            CalendarEvent ev = calendarServiceInstance().getEventById(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                int calType = Calendar.TYPE_ALL;
                try {
                    calType = Integer.parseInt(ev.getCalType());
                } catch (NumberFormatException e) {
                    calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), ev.getCalendarId());
                }
                switch (calType) {
                case Calendar.TYPE_PRIVATE:
                    calendarServiceInstance().removeUserEvent(currentUserId(), ev.getCalendarId(), id);
                    break;
                case Calendar.TYPE_PUBLIC:
                    calendarServiceInstance().removePublicEvent(ev.getCalendarId(), id);
                    break;
                case Calendar.TYPE_SHARED:
                    calendarServiceInstance().removeSharedEvent(currentUserId(), ev.getCalendarId(), id);
                    break;

                default:
                    break;
                }
                return Response.ok().cacheControl(nc).build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns an attachment with specified id if:
     * - the calendar of the event is public
     * - the authenticated user is the owner of the calendar of the event
     * - the authenticated user belongs to the group of the calendar of the event
     * - the authenticated user is a participant of the event
     * - the calendar of the event has been shared with the authenticated user or with a group of the authenticated user
     * 
     * @param id              
     *        identity of attachment to find
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/attachments/att123?fields=id,name
     * 
     * @format JSON
     * 
     * @response 
     *   {
     *      id: "att123",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/attachments/att123",
     *      name: "..", mimeType: "...",
     *      weight: ""
     *   } 
     *  
     * @return  return attachment info as json object
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getAttachmentById
     */
    @GET
    @RolesAllowed("users")
    @Path("/attachments/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getAttachmentById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo, @Context Request request) {
        try {
            id = AttachmentResource.decode(id);
            CalendarEvent ev = this.findEventAttachment(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            Attachment att = calendarServiceInstance().getAttachmentById(id);
            if (att == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Date lastModified = new Date(att.getLastModified());
            ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
            if (preCondition != null) {
                return preCondition.build();
            }

            boolean inParticipant = false;
            if (ev.getParticipant() != null) {
                String[] participant = ev.getParticipant();
                Arrays.sort(participant);
                int i = Arrays.binarySearch(participant, currentUserId());
                if (i > -1)
                    inParticipant = true;
            }

            if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())
                    || inParticipant) {

                AttachmentResource evData = new AttachmentResource(att, getBasePath(uriInfo));
                Object resource = extractObject(evData, fields);
                if (jsonp != null) {
                    String json = null;
                    if (resource instanceof Map)
                        json = new JSONObject(resource).toString();
                    else {
                        JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
                        json = generatorImpl.createJsonObject(resource).toString();
                    }
                    StringBuilder sb = new StringBuilder(jsonp);
                    sb.append("(").append(json).append(");");
                    return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(cc)
                            .lastModified(lastModified).build();
                }

                //
                return Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(cc).lastModified(lastModified)
                        .build();
            }

            //
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Deletes an attachment with specified id if:
     * - the authenticated user is the owner of the calendar of the event
     * - for group calendars, the authenticated user has edit rights on the calendar
     * - the calendar of the event has been shared with the authenticated user, with modification rights
     * - the calendar of the event has been shared with a group of the authenticated user, with modification rights
     * 
     * @param id  
     *        identity of the attachment to delete
     * 
     * @request  DELETE: http://localhost:8080/portal/rest/v1/calendar/attachments/att123
     * 
     * @response  HTTP status code:
     *            200 if delete successfully, 404 if event that contains attachment doen't exists
     *            401 if user don't have permission, 503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.deleteAttachmentById
     */
    @DELETE
    @RolesAllowed("users")
    @Path("/attachments/{id}")
    public Response deleteAttachmentById(@PathParam("id") String id) {
        try {
            id = AttachmentResource.decode(id);
            CalendarEvent ev = this.findEventAttachment(id);
            if (ev == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
            if (cal == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            if (Utils.isCalendarEditable(currentUserId(), cal)) {
                calendarServiceInstance().removeAttachmentById(id);
                return Response.ok().cacheControl(nc).build();
            }
            return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns the categories if an user is authenticated (the common categories + the personal categories)
     *
     * @param offset       
     *        The starting point when paging through a list of entities. Defaults to *0*.
     * 
     * @param limit         
     *        The maximum number of results when paging through a list of entities, if not specify or exceed
     *        the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *        (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     * 
     * @param fields        
     *        This is a list of comma separated property"s names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/categories?fields=id,name
     * 
     * @format JSON
     * 
     * @response 
     * [
     *   {
     *      id: "myCat",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/categories/myCat",
     *      name: ".." 
     *   }, 
     *   {id...}
     * ]
     * 
     * @return  List of event categories
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getEventCategories
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @GET
    @RolesAllowed("users")
    @Path("/categories")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getEventCategories(@QueryParam("offset") int offset, @QueryParam("limit") int limit,
            @QueryParam("fields") String fields, @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo) {
        limit = parseLimit(limit);

        try {
            List<EventCategory> ecData = calendarServiceInstance().getEventCategories(currentUserId(), offset,
                    limit);
            if (ecData == null || ecData.isEmpty())
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            Collection data = new ArrayList();

            String basePath = getBasePath(uriInfo);
            for (EventCategory ec : ecData) {
                data.add(extractObject(new CategoryResource(ec, basePath), fields));
            }

            CollectionResource resource = new CollectionResource(data, ecData.size());
            resource.setOffset(offset);
            resource.setLimit(limit);

            if (jsonp != null) {
                JsonValue json = new JsonGeneratorImpl().createJsonObject(resource);
                StringBuilder sb = new StringBuilder(jsonp);
                sb.append("(").append(json).append(");");
                return Response.ok(sb.toString(), new MediaType("text", "javascript"))
                        .header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, resource.getSize()))
                        .cacheControl(nc).build();
            }

            //
            return Response.ok(resource, MediaType.APPLICATION_JSON)
                    .header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, resource.getSize())).cacheControl(nc)
                    .build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns the event category by id and it belongs to user
     * 
     * @param id              
     *        identity of event category to find
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/categories/cat123?fields=id,name
     * 
     * @format JSON
     * 
     * @response 
     *   {
     *      id: "cat123",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/categories/cat123",
     *      name: ".." 
     *   }
     *  
     * @return  return event category info as json object
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getEventCategoryById
     */
    @GET
    @RolesAllowed("users")
    @Path("/categories/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getEventCategoryById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo, @Context Request request) {
        try {
            List<EventCategory> data = calendarServiceInstance().getEventCategories(currentUserId());
            if (data == null || data.isEmpty()) {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }
            EventCategory category = null;
            for (int i = 0; i < data.size(); i++) {
                if (id.equals(data.get(i).getId())) {
                    category = data.get(i);
                    break;
                }
            }

            if (category == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            Date lastModified = new Date(category.getLastModified());
            ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
            if (preCondition != null) {
                return preCondition.build();
            }

            CategoryResource categoryR = new CategoryResource(category, getBasePath(uriInfo));
            Object resource = extractObject(categoryR, fields);
            if (jsonp != null) {
                String json = null;
                if (resource instanceof Map)
                    json = new JSONObject((Map<?, ?>) resource).toString();
                else {
                    JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
                    json = generatorImpl.createJsonObject(resource).toString();
                }
                StringBuilder sb = new StringBuilder(jsonp);
                sb.append("(").append(json).append(");");
                return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(cc)
                        .lastModified(lastModified).build();
            }

            //
            return Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(cc).lastModified(lastModified)
                    .build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     *
     * Gets a feed with the given id    
     * Returns the feed if the authenticated user is the owner of the feed
     *  
     * @param id              
     *        title of feed to find
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=calendar. In case of collections, 
     *        you can put offset (default: 0), limit (default: *query_limit* of the rest service) value into param, for example: expand=calendar(0,5).
     *        Instead of: 
     *        {
     *            id: "...", 
     *            calendars: {
     *                        "http://localhost:8080/portal/rest/v1/calendar/calendars/demo-defaultCalendarId"
     *                        }
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            calendars: {
     *                id: "...",
     *                name:"demo-defaultId",
     *            ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/feeds/feed123?fields=id,name
     * 
     * @format JSON
     * 
     * @response 
     *   {
     *      id: "feed123",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/feeds/feed123",
     *      name: "..", rss: "...",
     *      calendars: ["...", ".."]
     *   }
     *  
     * @return  return feed as json object
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getFeedById
     */
    @GET
    @RolesAllowed("users")
    @Path("/feeds/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getFeedById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("expand") String expand, @QueryParam("jsonp") String jsonp, @Context UriInfo uriInfo,
            @Context Request request) {
        try {
            FeedData feed = null;
            for (FeedData feedData : calendarServiceInstance().getFeeds(currentUserId())) {
                if (feedData.getTitle().equals(id)) {
                    feed = feedData;
                    break;
                }
            }
            if (feed == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            byte[] data = feed.getContent();

            byte[] hashCode = digest(data).getBytes();
            EntityTag tag = new EntityTag(new String(hashCode));
            ResponseBuilder preCondition = request.evaluatePreconditions(tag);
            if (preCondition != null) {
                return preCondition.build();
            }

            SyndFeedInput input = new SyndFeedInput();
            SyndFeed syndFeed = input.build(new XmlReader(new ByteArrayInputStream(data)));
            List<SyndEntry> entries = new ArrayList<SyndEntry>(syndFeed.getEntries());
            List<String> calIds = new ArrayList<String>();
            for (SyndEntry entry : entries) {
                String calendarId = entry.getLink().substring(entry.getLink().lastIndexOf("/") + 1);
                calIds.add(calendarId);
            }

            Object resource = buildFeedResource(feed, calIds, uriInfo, expand, fields);
            return buildJsonP(resource, jsonp).cacheControl(cc).tag(tag).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Updates a feed with the given id   
     * Updates the feed if the authenticated user is the owner of the feed 
     * 
     * This entry point only allow http PUT request, with json object (feedResource) in the request body, and feed id in the path.
     * All the attributes of json object are optional, any omited attributes, or non-exists one will be ignored. *id* and *href* attributes can't
     * be updated, they also be ignored.
     * For example:
     *   {
     *      name: "..",
     *      calendarIds: ["...", ".."]
     *   }
     *  
     * @param id             
     *        title of the feed to update
     * 
     * @param feedResource  
     *        json object contains attributes of feed object to update, all the attributes are optional 
     * 
     * @request  PUT: http://localhost:8080/portal/rest/v1/calendar/feeds/feed123
     * 
     * @response  HTTP status code:
     *            200 if updated successfully, 404 if feed with provided id doesnt exists,
     *            503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.updateFeedById
     */
    @PUT
    @RolesAllowed("users")
    @Path("/feeds/{id}")
    public Response updateFeedById(@PathParam("id") String id, FeedResource feedResource) {
        try {
            FeedData feed = null;
            for (FeedData feedData : calendarServiceInstance().getFeeds(currentUserId())) {
                if (feedData.getTitle().equals(id)) {
                    feed = feedData;
                    break;
                }
            }

            if (feed == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            LinkedHashMap<String, Calendar> calendars = new LinkedHashMap<String, Calendar>();
            if (feedResource.getCalendarIds() != null) {
                for (String calendarId : feedResource.getCalendarIds()) {
                    Calendar calendar = calendarServiceInstance().getCalendarById(calendarId);
                    int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), calendarId);
                    switch (calType) {
                    case Calendar.TYPE_PRIVATE:
                        calendars.put(Calendar.TYPE_PRIVATE + Utils.COLON + calendarId, calendar);
                        break;
                    case Calendar.TYPE_PUBLIC:
                        calendars.put(Calendar.TYPE_PUBLIC + Utils.COLON + calendarId, calendar);
                        break;
                    case Calendar.TYPE_SHARED:
                        calendars.put(Calendar.TYPE_SHARED + Utils.COLON + calendarId, calendar);
                        break;
                    default:
                        break;
                    }
                }
            }

            //
            calendarServiceInstance().removeFeedData(currentUserId(), id);

            RssData rssData = new RssData();
            if (feedResource.getName() != null) {
                rssData.setName(feedResource.getName() + Utils.RSS_EXT);
                rssData.setTitle(feedResource.getName());
                rssData.setDescription(feedResource.getName());
            }
            rssData.setUrl(feed.getUrl());
            rssData.setLink(feed.getUrl());
            rssData.setVersion("rss_2.0");
            //
            calendarServiceInstance().generateRss(currentUserId(), calendars, rssData);

            return Response.ok().cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Deletes a feed with the given id    
     * Deletes the feed if the authenticated user is the owner of the feed 
     * 
     * @param id  
     *        title of the feed to delete
     * 
     * @request  DELETE: http://localhost:8080/portal/rest/v1/calendar/feeds/feed123
     * 
     * @response  HTTP status code:
     *            200 if delete successfully,
     *            503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.deleteFeedById
     */
    @DELETE
    @RolesAllowed("users")
    @Path("/feeds/{id}")
    public Response deleteFeedById(@PathParam("id") String id) {
        try {
            calendarServiceInstance().removeFeedData(currentUserId(), id);
            return Response.ok().cacheControl(nc).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     *  Gets the RSS stream of the feed with the given id
     *  Returns the RSS stream if:
     *   - the calendar is public
     *   - the authenticated user is the owner of the calendar
     *   - the authenticated user belongs to the group of the calendar
     *   - the calendar has been shared with the authenticated user or with a group of the authenticated user
     *   
     * @param id              
     *        title of the feed 
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/feeds/feed123/rss
     * 
     * @format application/xml
     * 
     * @response rss
     *  
     * @return  return rss from feed
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getRssFromFeed
     */
    @GET
    @RolesAllowed("users")
    @Path("/feeds/{id}/rss")
    @Produces(MediaType.APPLICATION_XML)
    public Response getRssFromFeed(@PathParam("id") String id, @Context UriInfo uri, @Context Request request) {
        try {
            String username = currentUserId();
            String feedname = id;
            FeedData feed = null;
            for (FeedData feedData : calendarServiceInstance().getFeeds(username)) {
                if (feedData.getTitle().equals(feedname)) {
                    feed = feedData;
                    break;
                }
            }

            if (feed == null)
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

            SyndFeedInput input = new SyndFeedInput();
            SyndFeed syndFeed = input.build(new XmlReader(new ByteArrayInputStream(feed.getContent())));
            List<SyndEntry> entries = new ArrayList<SyndEntry>(syndFeed.getEntries());
            List<CalendarEvent> events = new ArrayList<CalendarEvent>();
            List<Calendar> calendars = new ArrayList<Calendar>();
            for (SyndEntry entry : entries) {
                String calendarId = entry.getLink().substring(entry.getLink().lastIndexOf("/") + 1);
                calendars.add(calendarServiceInstance().getCalendarById(calendarId));
            }

            for (Calendar cal : calendars) {
                if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, username)) {
                    int calType = calendarServiceInstance().getTypeOfCalendar(username, cal.getId());
                    switch (calType) {
                    case Calendar.TYPE_PRIVATE:
                        events.addAll(calendarServiceInstance().getUserEventByCalendar(username,
                                Arrays.asList(cal.getId())));
                        break;
                    case Calendar.TYPE_SHARED:
                        events.addAll(calendarServiceInstance().getSharedEventByCalendars(username,
                                Arrays.asList(cal.getId())));
                        break;
                    case Calendar.TYPE_PUBLIC:
                        EventQuery eventQuery = new EventQuery();
                        eventQuery.setCalendarId(new String[] { cal.getId() });
                        events.addAll(calendarServiceInstance().getPublicEvents(eventQuery));
                        break;
                    default:
                        break;
                    }
                }
            }

            if (events.size() == 0) {
                return Response.status(HTTPStatus.NOT_FOUND).entity("Feed " + feedname + "is removed")
                        .cacheControl(nc).build();
            }
            String xml = makeFeed(username, events, feed, uri);

            byte[] hashCode = digest(xml.getBytes()).getBytes();
            EntityTag tag = new EntityTag(new String(hashCode));
            ResponseBuilder preCondition = request.evaluatePreconditions(tag);
            if (preCondition != null) {
                return preCondition.build();
            }

            return Response.ok(xml, MediaType.APPLICATION_XML).cacheControl(cc).tag(tag).build();
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug(e.getMessage());
        }
        return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
    }

    /**
     * Returns an invitation with specified id if:
     * - the authenticated user is the participant of the invitation
     * - the authenticated user has edit rights on the calendar of the event of the invitation
     * 
     * @param id              
     *        identity of invitation to find
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=event
     *        Instead of: 
     *        {
     *            id: "...", 
     *            event: "http://localhost:8080/portal/rest/v1/calendar/events/evt123"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            event: {
     *                id: "...",
     *                name:"myEvent",
     *                ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/invitations/evt123:root
     * 
     * @format  JSON
     * 
     * @response 
     *   {
     *      id: "evt123:root",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/events/evt123",
     *      event: "..", participant: "...",
     *      status: ""
     *   }
     *  
     * @return  invitation as json object
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.getInvitationById
     */
    @GET
    @RolesAllowed("users")
    @Path("/invitations/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getInvitationById(@PathParam("id") String id, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @QueryParam("expand") String expand, @Context UriInfo uriInfo,
            @Context Request request) throws Exception {
        CalendarService service = calendarServiceInstance();
        EventDAO evtDAO = service.getEventDAO();
        String username = currentUserId();

        Invitation invitation = evtDAO.getInvitationById(id);
        if (invitation == null)
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

        EntityTag tag = new EntityTag(String.valueOf(invitation.hashCode()));
        ResponseBuilder preCondition = request.evaluatePreconditions(tag);
        if (preCondition != null) {
            return preCondition.build();
        }

        //dont return invitation if user is not participant and not have edit permission
        if (!username.equals(invitation.getParticipant())) {
            CalendarEvent event = service.getEventById(invitation.getEventId());
            Calendar calendar = service.getCalendarById(event.getCalendarId());

            if (!Utils.isCalendarEditable(username, calendar)) {
                return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
            }
        }

        Object resource = buildInvitationResource(invitation, uriInfo, expand, fields);
        return buildJsonP(resource, jsonp).cacheControl(cc).tag(tag).build();
    }

    /**
     * Update the invitation if the authenticated user is the participant of the invitation
     * This entry point only allow http PUT request, with id of invitation in the path, and the status
     *  
     * @param id             
     *        identity of the invitation to update
     * 
     * @param status      
     *        new status to update ("", "maybe", "yes", "no")
     * 
     * @request  PUT: http://localhost:8080/portal/rest/v1/calendar/invitations/evt123:root
     * 
     * @response  HTTP status code:
     *            200 if updated successfully,
     *            404 if invitation with provided id doesnt exists,
     *            400 if status is not valid,
     *            401 if user don't have create permission, 503 if there is any error during the save process
     * 
     * @return  status code
     * 
     * @authentication
     *  
     * @anchor CalendarRestApi.updateInvitationById
     */
    @PUT
    @RolesAllowed("users")
    @Path("/invitations/{id}")
    public Response updateInvitationById(@PathParam("id") String id, @QueryParam("status") String status) {
        if (Arrays.binarySearch(INVITATION_STATUS, status) < 0) {
            return buildBadResponse(new ErrorResource(
                    "status must be one of: " + StringUtils.join(INVITATION_STATUS, ","), "status"));
        }
        CalendarService service = calendarServiceInstance();
        EventDAO evtDAO = service.getEventDAO();
        String username = currentUserId();

        Invitation invitation = evtDAO.getInvitationById(id);
        if (invitation != null) {
            //Update only if user is participant
            if (invitation.getParticipant().equals(username)) {
                evtDAO.updateInvitation(id, status);
                return Response.ok().cacheControl(nc).build();
            } else {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }
        } else {
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
        }
    }

    /**
     * Delete an invitation with specified id if the authenticated user has edit rights on the calendar of the event of the invitation
     *  
     * @param id 
     *        identity of the invitation to delete
     * 
     * @request  DELETE: http://localhost:8080/portal/rest/v1/calendar/invitations/evt123:root
     * 
     * @response  HTTP status code:
     *            200 if delete successfully,
     *            404 if invitation with provided id doesnt exists,
     *            401 if user don't have permission,
     *            503 if there is any error during the save process
     * 
     * @return status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.deleteInvitationById
     */
    @DELETE
    @RolesAllowed("users")
    @Path("/invitations/{id}")
    public Response deleteInvitationById(@PathParam("id") String id) throws Exception {
        CalendarService calService = calendarServiceInstance();
        EventDAO evtDAO = calService.getEventDAO();
        String username = currentUserId();

        Invitation invitation = evtDAO.getInvitationById(id);
        if (invitation == null)
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();

        CalendarEvent event = calService.getEventById(invitation.getEventId());
        Calendar calendar = calService.getCalendarById(event.getCalendarId());

        if (Utils.isCalendarEditable(username, calendar)) {
            evtDAO.removeInvitation(id);
            return Response.ok().cacheControl(nc).build();
        } else {
            return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
        }
    }

    /**
     * Returns invitations of an event with specified id when:
     * the authenticated user is the participant of the invitation
     * the authenticated user has edit rights on the calendar of the event of the invitation
     * 
     * @param id         
     *        identity of event to search for invitations
     *
     * @param status           
     *        search for this status only. If not specify, search invitation of any status ("", "maybe", "yes", "no")
     * 
     * @param offset            
     *        The starting point when paging through a list of entities. Defaults to *0*.
     * 
     * @param limit              
     *        The maximum number of results when paging through a list of entities, if not specify or exceed
     *        the *query_limit* configuration of calendar rest service, it will use the *query_limit* 
     *        (see more on {@link #CalendarRestApi(OrganizationService, InitParams)} java doc)
     * 
     * @param fields        
     *        This is a list of comma separated property's names of response json object,
     *        if not specified, it return the json will all available properties.
     * 
     * @param jsonp        
     *        The name of a JavaScript function to be used as the JSONP callback, if not specified, only
     *        json object is returned.
     * 
     * @param expand     
     *        used to ask for a full representation of a subresource, instead of only its link. 
     *        This is a list of comma-separated property's names. For example: expand=event
     *        Instead of: 
     *        {
     *            id: "...", 
     *            event: "http://localhost:8080/portal/rest/v1/calendar/events/evt123"
     *            ....
     *        }
     *        It returns:
     *        {
     *            id: "...", 
     *            event: {
     *                id: "...",
     *                name:"myEvent",
     *                ....
     *            }
     *            ....
     *        }
     * 
     * @request  GET: http://localhost:8080/portal/rest/v1/calendar/events/evt123/invitations
     * 
     * @format  JSON
     * 
     * @response 
     * [
     *   {
     *      id: "evt123:root",
     *      href: "http://localhost:8080/portal/rest/v1/calendar/events/evt123",
     *      event: "..", participant: "...",
     *      status: ""
     *   }, 
     *   {id...}
     * ]
     * 
     * @return  List of invitations of a specific event
     * 
     * @authentication
     *  
     * @anchor  CalendarRestApi.getInvitationsFromEvent
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @GET
    @RolesAllowed("users")
    @Path("/events/{id}/invitations/")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getInvitationsFromEvent(@PathParam("id") String id, @QueryParam("offset") int offset,
            @QueryParam("limit") int limit, @QueryParam("returnSize") boolean returnSize,
            @QueryParam("status") String status, @QueryParam("fields") String fields,
            @QueryParam("jsonp") String jsonp, @QueryParam("expand") String expand, @Context UriInfo uriInfo)
            throws Exception {
        limit = parseLimit(limit);
        CalendarService calService = calendarServiceInstance();

        CalendarEvent event = calService.getEventById(id);
        String username = currentUserId();

        List<Invitation> invitations = Collections.<Invitation>emptyList();
        if (event != null) {
            //All invitations in event
            invitations = new LinkedList<Invitation>(Arrays.asList(event.getInvitations()));

            //Only return user's invitation if calendar is not editable
            Calendar calendar = calService.getCalendarById(event.getCalendarId());
            if (!Utils.isCalendarEditable(username, calendar)) {
                Iterator<Invitation> iter = invitations.iterator();

                while (iter.hasNext()) {
                    if (!iter.next().getParticipant().equals(username)) {
                        iter.remove();
                    }
                }
            }

            //Return only invitation with specific status
            if (status != null) {
                Iterator<Invitation> iter = invitations.iterator();
                while (iter.hasNext()) {
                    if (!iter.next().getStatus().equals(status)) {
                        iter.remove();
                    }
                }
            }
        }

        List data = new LinkedList();
        for (Invitation invitation : Utils.subList(invitations, offset, limit)) {
            data.add(buildInvitationResource(invitation, uriInfo, expand, fields));
        }
        int fullSize = invitations.size();

        CollectionResource ivData = new CollectionResource(data, returnSize ? fullSize : -1);
        ivData.setOffset(offset);
        ivData.setLimit(limit);

        ResponseBuilder response = buildJsonP(ivData, jsonp);
        if (returnSize) {
            response.header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, fullSize));
        }
        return response.build();
    }

    /**
     *  Creates an invitation in the event with the given id . Creates the invitation only if:
     *  - the authenticated user is the participant of the invitation
     *  - the authenticated user has edit rights on the calendar of the event of the invitation
     * 
     * @param id                       
     *        identity of the *event* to create invitation
     * 
     * @param participant        
     *        name of participant (userId). If not provided or empty, return 400 status code
     * 
     * @param status               
     *        status of invitation ("", "maybe", "yes", "no")
     * 
     * @request  POST: http://localhost:8080/portal/rest/v1/calendar/events/evt123/invitations
     * 
     * @response  HTTP status code:
     *            201 if created successfully, and http header *location* href point to the newly created event.
     *            400 if participant or status is not valid.
     *            404 if event not found with provided id.
     *            401 if user don't have create permission,
     *            503 if there is any error during the save process.
     * 
     * @return  http status code
     * 
     * @authentication
     * 
     * @anchor CalendarRestApi.createInvitationForEvent
     */
    @POST
    @RolesAllowed("users")
    @Path("/events/{id}/invitations/")
    public Response createInvitationForEvent(@PathParam("id") String id, InvitationResource invitation,
            @Context UriInfo uriInfo) throws Exception {
        if (invitation == null) {
            return buildBadResponse(new ErrorResource("Invitation information must not null", "invitation"));
        }
        String participant = invitation.getParticipant();
        String status = invitation.getStatus();
        if (participant == null || participant.trim().isEmpty()) {
            return buildBadResponse(new ErrorResource("participant must not null or empty", "participant"));
        }
        if (Arrays.binarySearch(INVITATION_STATUS, status) < 0) {
            return buildBadResponse(new ErrorResource(
                    "status must be one of: " + StringUtils.join(INVITATION_STATUS, ","), "status"));
        }

        CalendarService service = calendarServiceInstance();
        EventDAO evtDAO = service.getEventDAO();
        String username = currentUserId();

        CalendarEvent event = service.getEventById(id);
        if (event != null) {
            Calendar calendar = service.getCalendarById(event.getCalendarId());
            if (!Utils.isCalendarEditable(username, calendar)) {
                return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
            }

            Invitation invite = evtDAO.createInvitation(id, participant, status);
            if (invite != null) {
                String location = new StringBuilder(getBasePath(uriInfo)).append(INVITATION_URI)
                        .append(invite.getId()).toString();
                return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc)
                        .build();
            } else {
                return buildBadResponse(new ErrorResource(
                        participant + " has already been participant, can't create more", "participant"));
            }
        } else {
            return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
        }
    }

    private Response buildBadResponse(ErrorResource error) {
        return Response.status(HTTPStatus.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON)
                .cacheControl(nc).build();
    }

    /**
     * Parse date by ISO8601 standard
     * if start is null, start is current time
     * if end is null, end is current time plus 1 week
     * @param start
     * @param end
     * @return array of start, end date
     */
    private java.util.Calendar[] parseDate(String start, String end) {
        java.util.Calendar from = GregorianCalendar.getInstance();
        java.util.Calendar to = GregorianCalendar.getInstance();
        if (Utils.isEmpty(start)) {
            from = java.util.Calendar.getInstance();
            from.set(java.util.Calendar.HOUR, 0);
            from.set(java.util.Calendar.MINUTE, 0);
            from.set(java.util.Calendar.SECOND, 0);
            from.set(java.util.Calendar.MILLISECOND, 0);
        } else {
            from = ISO8601.parse(start);
        }
        if (Utils.isEmpty(end)) {
            to.add(java.util.Calendar.WEEK_OF_MONTH, 1);
            to.set(java.util.Calendar.HOUR, 0);
            to.set(java.util.Calendar.MINUTE, 0);
            to.set(java.util.Calendar.SECOND, 0);
            to.set(java.util.Calendar.MILLISECOND, 0);
        } else {
            to = ISO8601.parse(end);
        }
        return new java.util.Calendar[] { from, to };
    }

    /**
     * Doesn't allow limit parameter to exceed the default query_limit
     */
    private int parseLimit(int limit) {
        return (limit <= 0 || limit > query_limit) ? query_limit : limit;
    }

    private String getBasePath(UriInfo uriInfo) {
        StringBuilder path = new StringBuilder(uriInfo.getBaseUri().toString());
        path.append(CAL_BASE_URI);
        return path.toString();
    }

    private String buildFullUrl(UriInfo uriInfo, int offset, int limit, long fullSize) {
        if (fullSize <= 0) {
            return "";
        }
        offset = offset < 0 ? 0 : offset;

        long prev = offset - limit;
        prev = offset > 0 && prev < 0 ? 0 : prev;
        long prevLimit = offset - prev;
        //
        StringBuilder sb = new StringBuilder();
        if (prev >= 0) {
            sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=");
            sb.append(prev).append("&limit=").append(prevLimit).append(">").append(Utils.SEMICOLON)
                    .append("rel=\"previous\",");
        }

        long next = offset + limit;
        //
        if (next < fullSize) {
            sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=");
            sb.append(next).append("&limit=").append(limit).append(">").append(Utils.SEMICOLON)
                    .append("rel=\"next\",");
        }

        //first page
        long firstLimit = limit > fullSize ? fullSize : limit;
        sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=0&limit=").append(firstLimit).append(">");
        sb.append(Utils.SEMICOLON).append("rel=\"first\",");
        //last page
        long lastIndex = fullSize - (fullSize % firstLimit);
        if (lastIndex == fullSize) {
            lastIndex = fullSize - firstLimit;
        }
        if (lastIndex > 0) {
            sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=").append(lastIndex);
            sb.append("&limit=").append(fullSize - lastIndex).append(">");
            sb.append(Utils.SEMICOLON).append("rel=\"last\"");
        }
        if (sb.charAt(sb.length() - 1) == ',') {
            sb.deleteCharAt(sb.length() - 1);
        }

        return sb.toString();
    }

    private static CalendarService calendarServiceInstance() {
        return (CalendarService) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(CalendarService.class);
    }

    /**
     * 
     * @param auhtor : the feed create
     * @param events : list of event from data
     * @return
     * @throws Exception
     */
    private String makeFeed(String author, List<CalendarEvent> events, FeedData feedData, UriInfo uri)
            throws Exception {
        URI baseUri = uri.getBaseUri();
        String baseURL = baseUri.getScheme() + "://" + baseUri.getHost() + ":"
                + Integer.toString(baseUri.getPort());
        String baseRestURL = baseUri.toString();

        SyndFeed feed = new SyndFeedImpl();
        feed.setFeedType("rss_2.0");
        feed.setTitle(feedData.getTitle());
        feed.setLink(baseURL + feedData.getUrl());
        feed.setDescription(feedData.getTitle());
        List<SyndEntry> entries = new ArrayList<SyndEntry>();
        SyndEntry entry;
        SyndContent description;
        for (CalendarEvent event : events) {
            if (Utils.EVENT_NUMBER > 0 && Utils.EVENT_NUMBER <= entries.size())
                break;
            entry = new SyndEntryImpl();
            entry.setTitle(event.getSummary());
            entry.setLink(baseRestURL + BASE_EVENT_URL + Utils.SLASH + author + Utils.SLASH + event.getId()
                    + Utils.SPLITTER + event.getCalType() + Utils.ICS_EXT);
            entry.setAuthor(author);
            description = new SyndContentImpl();
            description.setType(Utils.MIMETYPE_TEXTPLAIN);
            description.setValue(event.getDescription());
            entry.setDescription(description);
            entries.add(entry);
            entry.getEnclosures();
        }
        feed.setEntries(entries);
        feed.setEncoding("UTF-8");
        SyndFeedOutput output = new SyndFeedOutput();
        String feedXML = output.outputString(feed);
        feedXML = StringUtils.replace(feedXML, "&amp;", "&");
        return feedXML;
    }

    private boolean isInGroups(String[] groups) {
        Identity identity = ConversationState.getCurrent().getIdentity();
        for (String group : groups) {
            if (identity.isMemberOf(group)) {
                return true;
            }
        }

        return false;
    }

    private boolean hasViewCalendarPermission(Calendar cal, String username) throws Exception {
        if (cal.getCalendarOwner() != null && cal.getCalendarOwner().equals(username))
            return true;
        else if (cal.getGroups() != null) {
            return isInGroups(cal.getGroups());
        } else if (cal.getViewPermission() != null) {
            return Utils.canEdit(orgService, cal.getViewPermission(), username);
        }
        return false;
    }

    private List<Calendar> findViewableCalendars(String username) throws Exception {
        CalendarService service = calendarServiceInstance();
        //private calendar
        List<Calendar> uCals = service.getUserCalendars(username, true);
        //group calendar
        Set<String> groupIds = ConversationState.getCurrent().getIdentity().getGroups();
        List<GroupCalendarData> gCals = service.getGroupCalendars(groupIds.toArray(new String[groupIds.size()]),
                true, username);
        //shared calendar
        GroupCalendarData sCals = service.getSharedCalendars(username, true);
        if (sCals != null) {
            gCals.add(sCals);
        }
        //public calendar
        Calendar[] publicCals = service.getPublicCalendars().load(0, -1);

        List<Calendar> results = new LinkedList<Calendar>();
        results.addAll(Arrays.asList(publicCals));
        for (GroupCalendarData data : gCals) {
            if (data.getCalendars() != null) {
                for (Calendar cal : data.getCalendars()) {
                    results.add(cal);
                }
            }
        }
        results.addAll(uCals);

        return results;
    }

    private List<Calendar> findEditableCalendars(String username) throws Exception {
        List<Calendar> calendars = findViewableCalendars(username);
        Iterator<Calendar> iter = calendars.iterator();
        while (iter.hasNext()) {
            if (!Utils.isCalendarEditable(username, iter.next())) {
                iter.remove();
            }
        }
        return calendars;
    }

    private EventQuery buildEventQuery(String start, String end, String category, List<Calendar> calendars,
            String calendarPath, String participant, String eventType) {
        java.util.Calendar[] dates = parseDate(start, end);

        //Find all invitations that user is participant
        EventQuery uQuery = new RestEventQuery();
        uQuery.setQueryType(Query.SQL);
        uQuery.setCalendarPath(calendarPath);
        List<String> calIds = new LinkedList<String>();
        if (calendars != null) {
            for (Calendar cal : calendars) {
                calIds.add(cal.getId());
            }
            uQuery.setCalendarId(calIds.toArray(new String[calIds.size()]));
        }
        if (category != null) {
            uQuery.setCategoryId(new String[] { category });
        }
        if (participant != null) {
            uQuery.setParticipants(new String[] { participant });
        }
        uQuery.setEventType(eventType);
        uQuery.setFromDate(dates[0]);
        uQuery.setToDate(dates[1]);
        uQuery.setOrderType(Utils.ORDER_TYPE_ASCENDING);
        uQuery.setOrderBy(new String[] { Utils.ORDERBY_TITLE });
        return uQuery;
    }

    private CalendarEvent findEventAttachment(String attachmentID) throws Exception {
        int idx = attachmentID.indexOf("/calendars/");
        if (idx != -1) {
            int calendars = idx + "/calendars/".length();
            int calendar = attachmentID.indexOf('/', calendars) + 1;
            int event = attachmentID.indexOf('/', calendar);
            if (calendar != -1 && event != -1) {
                String eventId = attachmentID.substring(calendar, event);
                return calendarServiceInstance().getEventById(eventId);
            }
        }
        return null;
    }

    private Object extractObject(Resource iv, String fields) {
        if (fields != null && iv != null) {
            String[] f = fields.split(",");

            if (f.length > 0) {
                JSONObject obj = new JSONObject(iv);
                Map<String, Object> map = new HashMap<String, Object>();

                for (String name : f) {
                    try {
                        map.put(name, obj.get(name));
                    } catch (JSONException e) {
                        log.warn("Can't extract property {} from object {}", name, iv);
                    }
                }
                return map;
            }
        }
        return iv;
    }

    private String currentUserId() {
        return ConversationState.getCurrent().getIdentity().getUserId();
    }

    private Response buildEvent(CalendarEvent old, EventResource evObject) {
        String catId = evObject.getCategoryId();
        setEventCategory(old, catId);
        if (evObject.getDescription() != null) {
            old.setDescription(evObject.getDescription());
        }
        String eventState = evObject.getAvailability();
        if (eventState != null) {
            if (Arrays.binarySearch(EVENT_AVAILABILITY, eventState) < 0) {
                return buildBadResponse(new ErrorResource(
                        "availability must be one of " + StringUtils.join(EVENT_AVAILABILITY, ","),
                        "availability"));
            } else {
                old.setEventState(eventState);
            }
        }
        if (evObject.getRepeat() != null) {
            RepeatResource repeat = evObject.getRepeat();
            if (repeat.getExclude() != null) {
                old.setExceptionIds(Arrays.asList(repeat.getExclude()));
            }
            if (repeat.getRepeatOn() != null) {
                String[] reptOns = repeat.getRepeatOn().split(",");
                for (String on : reptOns) {
                    if (Arrays.binarySearch(RP_WEEKLY_BYDAY, on) < 0) {
                        return buildBadResponse(new ErrorResource(
                                "repeatOn can only contains " + StringUtils.join(RP_WEEKLY_BYDAY, ","),
                                "repeatOn"));
                    }
                }
                old.setRepeatByDay(reptOns);
            }
            if (repeat.getRepeateBy() != null) {
                String[] repeatBy = repeat.getRepeateBy().split(",");
                long[] by = new long[repeatBy.length];
                for (int i = 0; i < repeatBy.length; i++) {
                    try {
                        by[i] = Integer.parseInt(repeatBy[i]);
                        if (by[i] < 1 || by[i] > 31) {
                            return buildBadResponse(
                                    new ErrorResource("repeatBy must be >= 1 and <= 31", "repeatBy"));
                        }
                    } catch (Exception e) {
                    }
                }
                old.setRepeatByMonthDay(by);
            }

            if (repeat.getEnd() != null) {
                End end = repeat.getEnd();
                String val = end.getValue();
                if (val != null) {
                    try {
                        old.setRepeatUntilDate(ISO8601.parse(val).getTime());
                    } catch (Exception e) {
                        try {
                            old.setRepeatCount(Long.parseLong(end.getValue()));
                        } catch (Exception ex) {
                        }
                    }
                }
                String reptType = end.getType();
                if (reptType != null) {
                    if (Arrays.binarySearch(REPEATTYPES, reptType) < 0) {
                        return buildBadResponse(new ErrorResource(
                                "repeat type must be one of " + StringUtils.join(REPEATTYPES, ","), "end.type"));
                    } else {
                        old.setRepeatType(end.getType());
                    }
                }
            }

            int every = repeat.getEvery();
            if (every < 1 || every > 30) {
                every = 1;
            }
            old.setRepeatInterval(repeat.getEvery());
        }

        java.util.Calendar[] fromTo = parseDate(evObject.getFrom(), evObject.getTo());
        if (fromTo[0].after(fromTo[1]) || fromTo[0].equals(fromTo[1])) {
            return buildBadResponse(new ErrorResource("\"from\" date must be before \"to\" date", "from"));
        }
        old.setFromDateTime(fromTo[0].getTime());
        if (evObject.getLocation() != null) {
            old.setLocation(evObject.getLocation());
        }
        String priority = evObject.getPriority();
        if (priority != null) {
            if (Arrays.binarySearch(PRIORITY, priority) < 0) {
                return buildBadResponse(new ErrorResource(
                        "priority must be one of " + StringUtils.join(PRIORITY, ","), "priority"));
            } else {
                old.setPriority(evObject.getPriority());
            }
        }
        if (evObject.getReminder() != null) {
            old.setReminders(Arrays.asList(evObject.getReminder()));
        }
        String privacy = evObject.getPrivacy();
        if (privacy != null) {
            if (!CalendarEvent.IS_PRIVATE.equals(privacy) && !CalendarEvent.IS_PUBLIC.equals(privacy)) {
                return buildBadResponse(new ErrorResource("privacy can only be public or private", "privacy"));
            } else {
                old.setPrivate(CalendarEvent.IS_PRIVATE.equals(privacy));
            }
        }
        String subject = evObject.getSubject();
        if (subject != null) {
            subject = subject.trim();
            if (subject.isEmpty()) {
                return buildBadResponse(new ErrorResource("subject must not be empty", "subject"));
            } else {
                old.setSummary(subject);
            }
        }
        old.setToDateTime(fromTo[1].getTime());
        return null;
    }

    private Response buildEventFromTask(CalendarEvent old, TaskResource evObject) {
        String catId = evObject.getCategoryId();
        setEventCategory(old, catId);
        if (evObject.getNote() != null) {
            old.setDescription(evObject.getNote());
        }
        java.util.Calendar[] fromTo = parseDate(evObject.getFrom(), evObject.getTo());
        if (fromTo[0].after(fromTo[1]) || fromTo[0].equals(fromTo[1])) {
            return buildBadResponse(new ErrorResource("\"from\" date must be before \"to\" date", "from"));
        }
        old.setFromDateTime(fromTo[0].getTime());
        String priority = evObject.getPriority();
        if (priority != null) {
            if (Arrays.binarySearch(PRIORITY, priority) < 0) {
                return buildBadResponse(new ErrorResource(
                        "priority must be one of " + StringUtils.join(PRIORITY, ","), "priority"));
            } else {
                old.setPriority(evObject.getPriority());
            }
        }
        if (evObject.getReminder() != null) {
            old.setReminders(Arrays.asList(evObject.getReminder()));
        }
        String status = evObject.getStatus();
        if (status != null && !status.isEmpty()) {
            if (Arrays.binarySearch(TASK_STATUS, status) < 0) {
                return buildBadResponse(
                        new ErrorResource("status must be one of " + StringUtils.join(TASK_STATUS, ","), "status"));
            } else {
                old.setStatus(status);
            }
        }
        String name = evObject.getName();
        if (name != null) {
            name = name.trim();
            if (name.isEmpty()) {
                return buildBadResponse(new ErrorResource("name must not be empty", "name"));
            } else {
                old.setSummary(evObject.getName());
            }
        }
        old.setToDateTime(fromTo[1].getTime());
        if (evObject.getDelegation() != null) {
            old.setTaskDelegator(StringUtils.join(evObject.getDelegation(), ","));
        }
        return null;
    }

    private void setEventCategory(CalendarEvent old, String catId) {
        if (catId != null) {
            try {
                EventCategory cat = calendarServiceInstance().getEventCategory(currentUserId(), catId);
                if (cat != null) {
                    old.setEventCategoryId(cat.getId());
                    old.setEventCategoryName(cat.getName());
                }
            } catch (Exception e) {
                log.debug(e.getMessage(), e);
            }
        }
    }

    private Response buildCalendar(Calendar cal, CalendarResource calR) {
        if (calR.getColor() != null) {
            cal.setCalendarColor(calR.getColor());
        }
        if (calR.getOwner() != null) {
            cal.setCalendarOwner(calR.getOwner());
        }
        if (calR.getDescription() != null) {
            cal.setDescription(calR.getDescription());
        }
        if (calR.getEditPermission() != null) {
            cal.setEditPermission(calR.getEditPermission().split(Utils.SEMICOLON));
        }
        if (calR.getGroups() != null) {
            cal.setGroups(calR.getGroups());
        }
        String name = calR.getName();
        if (name != null) {
            name = name.trim();
            if (name.isEmpty() || containSpecialChar(name)) {
                return buildBadResponse(
                        new ErrorResource("calendar name is empty or contains special characters", "name"));
            } else {
                cal.setName(calR.getName());
            }
        }
        if (calR.getPrivateURL() != null) {
            cal.setPrivateUrl(calR.getPrivateURL());
        }
        if (calR.getPublicURL() != null) {
            cal.setPublicUrl(calR.getPublicURL());
        }
        if (calR.getTimeZone() != null) {
            cal.setTimeZone(calR.getTimeZone());
        }
        if (calR.getViewPermision() != null) {
            cal.setViewPermission(calR.getViewPermision().split(Utils.SEMICOLON));
        }
        return null;
    }

    private boolean containSpecialChar(String value) {
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            if (Character.isLetter(c) || Character.isDigit(c) || c == '_' || c == '-' || Character.isSpaceChar(c)) {
                continue;
            }
            return true;
        }
        return false;
    }

    private ResponseBuilder buildJsonP(Object resource, String jsonp) throws Exception {
        ResponseBuilder response = null;
        if (jsonp != null) {
            String json = null;
            if (resource instanceof Map)
                json = new JSONObject((Map<?, ?>) resource).toString();
            else {
                JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
                json = generatorImpl.createJsonObject(resource).toString();
            }
            StringBuilder sb = new StringBuilder(jsonp);
            sb.append("(").append(json).append(");");
            response = Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(nc);
        } else {
            response = Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(nc);
        }

        return response;
    }

    private Object buildTaskResource(CalendarEvent event, UriInfo uriInfo, String expand, String fields)
            throws Exception {
        CalendarService service = calendarServiceInstance();
        String basePath = getBasePath(uriInfo);
        TaskResource evtResource = new TaskResource(event, basePath);

        List<Expand> expands = Expand.parse(expand);
        for (Expand exp : expands) {
            if ("calendar".equals(exp.getField())) {
                Calendar cal = service.getCalendarById(event.getCalendarId());
                cal.setCalType(calendarServiceInstance().getTypeOfCalendar(currentUserId(), cal.getId()));
                evtResource.setCal(new CalendarResource(cal, basePath));
            } else if ("categories".equals(exp.getField())) {
                String categoryId = event.getEventCategoryId();
                if (categoryId != null) {
                    EventCategory evCat = service.getEventCategory(currentUserId(), categoryId);
                    if (evCat != null) {
                        CategoryResource[] catRs = new CategoryResource[] { new CategoryResource(evCat, basePath) };
                        evtResource
                                .setCats(Utils.<CategoryResource>subArray(catRs, exp.getOffset(), exp.getLimit()));
                    }
                }
            } else if ("attachments".equals(exp.getField())) {
                if (event.getAttachment() != null) {
                    List<AttachmentResource> attRs = new LinkedList<AttachmentResource>();
                    for (Attachment att : event.getAttachment()) {
                        attRs.add(new AttachmentResource(att, basePath));
                    }
                    attRs = Utils.subList(attRs, exp.getOffset(), exp.getLimit());
                    evtResource.setAtts(attRs.toArray(new AttachmentResource[attRs.size()]));
                }
            }
        }

        return extractObject(evtResource, fields);
    }

    private Object buildEventResource(CalendarEvent ev, UriInfo uriInfo, String expand, String fields)
            throws Exception {
        CalendarService service = calendarServiceInstance();
        String basePath = getBasePath(uriInfo);
        EventResource evtResource = new EventResource(ev, basePath);

        List<Expand> expands = Expand.parse(expand);
        for (Expand exp : expands) {
            if ("calendar".equals(exp.getField())) {
                Calendar cal = service.getCalendarById(ev.getCalendarId());
                cal.setCalType(calendarServiceInstance().getTypeOfCalendar(currentUserId(), cal.getId()));
                evtResource.setCal(new CalendarResource(cal, basePath));
            } else if ("categories".equals(exp.getField())) {
                String categoryId = ev.getEventCategoryId();
                if (categoryId != null) {
                    EventCategory evCat = service.getEventCategory(currentUserId(), categoryId);
                    if (evCat != null) {
                        CategoryResource[] catRs = new CategoryResource[] { new CategoryResource(evCat, basePath) };
                        evtResource
                                .setCats(Utils.<CategoryResource>subArray(catRs, exp.getOffset(), exp.getLimit()));
                    }
                }
            } else if ("originalEvent".equals(exp.getField())) {
                String orgId = ev.getOriginalReference();
                if (orgId != null) {
                    CalendarEvent orgEv = service.getEventById(orgId);
                    if (orgEv != null) {
                        evtResource.setOEvent(new EventResource(orgEv, basePath));
                    }
                }
            } else if ("attachments".equals(exp.getField())) {
                if (ev.getAttachment() != null) {
                    List<AttachmentResource> attRs = new LinkedList<AttachmentResource>();
                    for (Attachment att : ev.getAttachment()) {
                        attRs.add(new AttachmentResource(att, basePath));
                    }
                    attRs = Utils.subList(attRs, exp.getOffset(), exp.getLimit());
                    evtResource.setAtts(attRs.toArray(new AttachmentResource[attRs.size()]));
                }
            }
        }

        return extractObject(evtResource, fields);
    }

    private Object buildFeedResource(FeedData feed, List<String> calIds, UriInfo uriInfo, String expand,
            String fields) throws Exception {
        CalendarService service = calendarServiceInstance();
        String basePath = getBasePath(uriInfo);
        FeedResource feedResource = new FeedResource(feed, calIds.toArray(new String[calIds.size()]), basePath);

        List<Expand> expands = Expand.parse(expand);
        for (Expand exp : expands) {
            if ("calendars".equals(exp.getField())) {
                List<Serializable> calendars = new ArrayList<Serializable>();
                for (String calId : Utils.subList(calIds, exp.getOffset(), exp.getLimit())) {
                    calendars.add(new CalendarResource(service.getCalendarById(calId), getBasePath(uriInfo)));
                }
                feedResource.setCals(Utils.subList(calendars, exp.getOffset(), exp.getLimit()));
            }
        }

        return extractObject(feedResource, fields);
    }

    private Object buildInvitationResource(Invitation invitation, UriInfo uriInfo, String expand, String fields)
            throws Exception {
        CalendarService service = calendarServiceInstance();
        String basePath = getBasePath(uriInfo);
        InvitationResource ivtResource = new InvitationResource(invitation, basePath);

        List<Expand> expands = Expand.parse(expand);
        for (Expand exp : expands) {
            if ("event".equals(exp.getField())) {
                CalendarEvent event = service.getEventById(invitation.getEventId());
                ivtResource.setEvt(new EventResource(event, basePath));
            }
        }

        return extractObject(ivtResource, fields);
    }

    private String digest(byte[] data) throws Exception {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] hashCode = md5.digest(data);
        //Can't compile if return byte[] due to the bug from eXo rest framework
        return String.valueOf(hashCode);
    }

    public static class Expand {
        private String field;
        private int offset;
        private int limit;

        public Expand(String field, int offset, int limit) {
            this.field = field;
            this.offset = offset;
            this.limit = limit;
        }

        public static List<Expand> parse(String expand) {
            List<Expand> expands = new LinkedList<CalendarRestApi.Expand>();

            if (expand != null) {
                String[] frags = expand.split(",");
                List<String> tmp = new LinkedList<String>();
                for (int i = 0; i < frags.length; i++) {
                    String str = frags[i].trim();
                    if (!str.contains("(") && str.contains("")) {
                        tmp.add(str);
                    } else if (str.contains("(") && i + 1 < frags.length) {
                        tmp.add(str + "," + frags[++i]);
                    }
                }

                for (String exp : tmp) {
                    String fieldName = null;
                    int offset = 0;
                    int limit = -1;
                    if (exp != null) {
                        exp = exp.trim();
                        int i = exp.indexOf('(');
                        if (i > 0) {
                            fieldName = exp.substring(0, i).trim();
                            try {
                                offset = Integer.parseInt(
                                        exp.substring(exp.indexOf("offset:") + "offset:".length(), exp.indexOf(","))
                                                .trim());
                                limit = Integer.parseInt(
                                        exp.substring(exp.indexOf("limit:") + "limit:".length(), exp.indexOf(")"))
                                                .trim());
                            } catch (Exception ex) {
                            }
                        } else {
                            fieldName = exp;
                        }
                    }

                    expands.add(new Expand(fieldName, offset, limit));
                }
            }

            return expands;
        }

        public String getField() {
            return field;
        }

        public int getOffset() {
            return offset;
        }

        public int getLimit() {
            return limit;
        }
    }
}