View Javadoc
1   /*
2    * Copyright (C) 2003-2014 eXo Platform SAS.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program. If not, see <http://www.gnu.org/licenses/>.
16   */
17  
18  package org.exoplatform.calendar.ws;
19  
20  import com.sun.syndication.feed.synd.*;
21  import com.sun.syndication.io.SyndFeedInput;
22  import com.sun.syndication.io.SyndFeedOutput;
23  import com.sun.syndication.io.XmlReader;
24  import net.fortuna.ical4j.data.CalendarOutputter;
25  import net.fortuna.ical4j.model.property.CalScale;
26  import net.fortuna.ical4j.model.property.Method;
27  import net.fortuna.ical4j.model.property.ProdId;
28  import net.fortuna.ical4j.model.property.Version;
29  import org.apache.commons.fileupload.FileItem;
30  import org.apache.commons.lang.StringUtils;
31  import org.exoplatform.calendar.model.Event;
32  import org.exoplatform.calendar.service.*;
33  import org.exoplatform.calendar.service.Calendar;
34  import org.exoplatform.calendar.service.Calendar.Type;
35  import org.exoplatform.calendar.service.impl.MailNotification;
36  import org.exoplatform.calendar.ws.bean.*;
37  import org.exoplatform.calendar.ws.common.Resource;
38  import org.exoplatform.calendar.ws.common.RestAPIConstants;
39  import org.exoplatform.common.http.HTTPStatus;
40  import org.exoplatform.commons.utils.DateUtils;
41  import org.exoplatform.commons.utils.ISO8601;
42  import org.exoplatform.commons.utils.ListAccess;
43  import org.exoplatform.commons.utils.MimeTypeResolver;
44  import org.exoplatform.container.ExoContainerContext;
45  import org.exoplatform.container.xml.InitParams;
46  import org.exoplatform.container.xml.ValueParam;
47  import org.exoplatform.services.log.ExoLogger;
48  import org.exoplatform.services.log.Log;
49  import org.exoplatform.services.mail.MailService;
50  import org.exoplatform.services.organization.Group;
51  import org.exoplatform.services.organization.OrganizationService;
52  import org.exoplatform.services.rest.resource.ResourceContainer;
53  import org.exoplatform.services.security.ConversationState;
54  import org.exoplatform.services.security.Identity;
55  import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
56  import org.exoplatform.social.core.manager.IdentityManager;
57  import org.exoplatform.social.core.profile.ProfileFilter;
58  import org.exoplatform.upload.UploadResource;
59  import org.exoplatform.upload.UploadService;
60  import org.exoplatform.webservice.cs.bean.End;
61  import org.exoplatform.ws.frameworks.json.impl.JsonGeneratorImpl;
62  import org.exoplatform.ws.frameworks.json.value.JsonValue;
63  import org.json.JSONException;
64  import org.json.JSONObject;
65  
66  import javax.annotation.security.RolesAllowed;
67  import javax.jcr.query.Query;
68  import javax.ws.rs.*;
69  import javax.ws.rs.core.*;
70  import javax.ws.rs.core.Response.ResponseBuilder;
71  import java.io.ByteArrayInputStream;
72  import java.io.ByteArrayOutputStream;
73  import java.io.FileInputStream;
74  import java.io.InputStream;
75  import java.io.OutputStream;
76  import java.io.Serializable;
77  import java.net.URI;
78  import java.security.MessageDigest;
79  import java.util.*;
80  import java.util.stream.Collectors;
81  import java.util.stream.Stream;
82  
83  // Swagger //
84  import io.swagger.annotations.Api;
85  import io.swagger.annotations.ApiOperation;
86  import io.swagger.annotations.ApiParam;
87  import io.swagger.annotations.ApiResponse;
88  import io.swagger.annotations.ApiResponses;
89  
90  /**
91   * This rest service class provides entry point for calendar resources.
92   */
93  @Path(CalendarRestApi.CAL_BASE_URI)
94  @Api(value = CalendarRestApi.CAL_BASE_URI, description = "Entry point for calendar resources")
95  public class CalendarRestApi implements ResourceContainer {
96  
97    public final static String CAL_BASE_URI = RestAPIConstants.BASE_VERSION_URI + "/calendar";
98  
99    public static final String TEXT_ICS = "text/calendar";
100   public static final MediaType TEXT_ICS_TYPE = new MediaType("text","calendar");
101 
102   // TODO: Why /cs/calendar is still being used here ?
103   public final static String BASE_URL = "/cs/calendar";
104   public final static String BASE_EVENT_URL = BASE_URL + "/event";
105 
106 
107   public final static String CALENDAR_URI = "/calendars/";
108   public final static String EVENT_URI = "/events/";
109   public final static String TASK_URI = "/tasks/";
110   public final static String ICS_URI = "/ics";
111   public final static String ATTACHMENT_URI = "/attachments/";
112   public final static String OCCURRENCE_URI = "/occurrences";
113   public final static String CATEGORY_URI = "/categories/";
114   public final static String PARTICIPANT_URI = "/participants/";
115   public final static String AVAILABILITY_URI = "/availabilities/";
116   public final static String FEED_URI = "/feeds/";
117   public final static String RSS_URI = "/rss";
118   public final static String INVITATION_URI ="/invitations/";
119   public static final String HEADER_LINK = "Link";
120   public static final String HEADER_LOCATION = "Location";
121 
122   private OrganizationService orgService;
123   private IdentityManager identityManager;
124   private UploadService uploadService;
125   private MailService mailService;
126 
127   private int defaultLimit = 10;
128   private int hardLimit = 100;
129 
130   private SubResourceHrefBuilder subResourcesBuilder = new SubResourceHrefBuilder(this);
131 
132   private final static CacheControl nc = new CacheControl();
133 
134   public static final String DEFAULT_CAL_NAME = "calendar";
135 
136   public static final String DEFAULT_EVENT_NAME = "default";
137 
138   public static final String[] RP_WEEKLY_BYDAY = CalendarEvent.RP_WEEKLY_BYDAY.clone();
139 
140   public static final String[] EVENT_AVAILABILITY = {CalendarEvent.ST_AVAILABLE, CalendarEvent.ST_BUSY, CalendarEvent.ST_OUTSIDE};
141 
142   public static final String[] REPEATTYPES = CalendarEvent.REPEATTYPES.clone();
143 
144   public final static String RP_END_BYDATE = "endByDate";
145 
146   public final static String RP_END_AFTER = "endAfter";
147 
148   public final static String RP_END_NEVER = "neverEnd";
149 
150   public static final String[] PRIORITY = CalendarEvent.PRIORITY.clone();
151 
152   public static final String[] TASK_STATUS = CalendarEvent.TASK_STATUS.clone();
153 
154   private static final String[] INVITATION_STATUS = {"", "maybe", "yes", "no"};
155 
156   public static enum RecurringUpdateType {
157     ALL, FOLLOWING, ONE
158   }
159 
160   static {
161     Arrays.sort(RP_WEEKLY_BYDAY);
162     Arrays.sort(EVENT_AVAILABILITY);
163     Arrays.sort(REPEATTYPES);
164     Arrays.sort(PRIORITY);
165     Arrays.sort(TASK_STATUS);
166     Arrays.sort(INVITATION_STATUS);
167   }
168 
169   private final CacheControl cc = new CacheControl();
170 
171   static {
172     nc.setNoCache(true);
173     nc.setNoStore(true);
174   }
175 
176   private static final Log log = ExoLogger.getExoLogger(CalendarRestApi.class);
177 
178   /**
179    * Constructor helps to configure the rest service with parameters.
180    *
181    * Here is the configuration parameters:
182    * - default.limit      default number of objects returned by a collection query, default value: 10.
183    * - hard.limit         maximum number of objects returned by a collection query, default value: 100.
184    * - cache_maxage       time in milliseconds returned in the cache-control header, default value:  604800.
185    *
186    * @param  orgService
187    *         eXo organization service implementation.
188    *
189    * @param  params
190    *         Object contains the configuration parameters.
191    */
192   public CalendarRestApi(OrganizationService orgService, IdentityManager identityManager, UploadService uploadService, MailService mailService, InitParams params) {
193     this.orgService = orgService;
194     this.identityManager = identityManager;
195     this.uploadService = uploadService;
196     this.mailService = mailService;
197 
198     int maxAge = 604800;
199     if (params != null) {
200       if (params.getValueParam("default.limit") != null) {
201         defaultLimit = Integer.parseInt(params.getValueParam("default.limit").getValue());
202       }
203       if (params.getValueParam("hard.limit") != null) {
204         hardLimit = Integer.parseInt(params.getValueParam("hard.limit").getValue());
205       }
206 
207       ValueParam cacheConfig = params.getValueParam("cache_maxage");
208       if (cacheConfig != null) {
209         try {
210           maxAge = Integer.parseInt(cacheConfig.getValue());
211         } catch (Exception ex) {
212           log.warn("Can't parse {} to maxAge, use the default value {}", cacheConfig, maxAge);
213         }
214       }
215     }
216     cc.setPrivate(true);
217     cc.setMaxAge(maxAge);
218     cc.setSMaxAge(maxAge);
219   }
220 
221   /**
222    * Returns all the available sub-resources of this API in JSON.
223    *
224    * @request  GET: http://localhost:8080/rest/private/v1/calendar
225    *
226    * @format  JSON
227    *
228    * @response
229    *    {
230    *        "subResourcesHref": [
231    *            "http://localhost:8080/rest/private/v1/calendar/calendars",
232    *            "http://localhost:8080/rest/private/v1/calendar/events",
233    *            "http://localhost:8080/rest/private/v1/calendar/tasks"
234    *         ]
235    *     }
236    *
237    * @return  URLs of all REST services provided by this API, in absolute form.
238    *
239    * @authentication
240    *
241    * @anchor  CalendarRestApi.getSubResources
242    */
243   @GET
244   @RolesAllowed("users")
245   @Produces(MediaType.APPLICATION_JSON)
246   @ApiOperation(
247           value = "Returns all the available subresources as json",
248           notes = "Returns all the available subresources as json, in order to navigate easily in the REST API.")
249   @ApiResponses(value = {
250           @ApiResponse(code = 200, message = "Successful retrieval of all available subresources")
251   })
252   public Response getSubResources(@Context UriInfo uri) {
253     Map<String, String[]> subResources = new HashMap<String, String[]>();
254     subResources.put("subResourcesHref", subResourcesBuilder.buildResourceMap(uri));
255 
256     return Response.ok(subResources, MediaType.APPLICATION_JSON).cacheControl(nc).build();
257   }
258 
259   /**
260    * Searches for calendars by a type (personal/group/shared), returns calendars that the user has access permission.
261    *
262    * @param type Type of calendar, can be *personal*, *group* or *shared*. If omitted or unknown, it searches for *all* types.
263    *
264    * @param offset The starting point when paging the result. Default is *0*.
265    *
266    * @param limit Maximum number of calendars returned.
267    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
268    *
269    * @param returnSize Default is *false*. If set to *true*, the total number of matched calendars will be returned in JSON,
270    *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
271    *
272    * @param fields Comma-separated list of selective calendar attributes to be returned. All returned if not specified.
273    *
274    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
275    *        If not specified, only JSON object is returned.
276    *
277    * @request  {@code GET: http://localhost:8080/rest/private/v1/calendar/calendars?type=personal&fields=id,name}
278    *
279    * @format  JSON
280    *
281    * @response
282    * {
283    *   "limit": 10,
284    *   "data": [
285    *     {
286    *       "editPermission": "",
287    *       "viewPermission": "",
288    *       "privateURL": null,
289    *       "publicURL": null,
290    *       "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
291    *       "description": null,
292    *       "color": "asparagus",
293    *       "timeZone": "Europe/Brussels",
294    *       "name": "John Smith",
295    *       "type": "0",
296    *       "owner": "john",
297    *       "groups": null,
298    *       "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
299    *       "id": "john-defaultCalendarId"
300    *     },
301    *     {
302    *       "editPermission": "/platform/users/:*.*;",
303    *       "viewPermission": "*.*;",
304    *       "privateURL": null,
305    *       "publicURL": null,
306    *       "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/calendar8b8f65e77f00010122b425ec81b80da9/ics",
307    *       "description": null,
308    *       "color": "asparagus",
309    *       "timeZone": "Europe/Brussels",
310    *       "name": "Users",
311    *       "type": "0",
312    *       "owner": null,
313    *       "groups": [
314    *         "/platform/users"
315    *       ],
316    *       "href": "http://localhost:8080/rest/private/v1/calendar/calendars/calendar8b8f65e77f00010122b425ec81b80da9",
317    *       "id": "calendar8b8f65e77f00010122b425ec81b80da9"
318    *     }
319    *   ],
320    *   "size": -1,
321    *   "offset": 0
322    * }
323    *
324    * @return  List of calendars in JSON.
325    *
326    * @authentication
327    *
328    * @anchor  CalendarRestApi.getCalendars
329    */
330   @SuppressWarnings({ "unchecked", "rawtypes" })
331   @GET
332   @RolesAllowed("users")
333   @Path("/calendars/")
334   @Produces({MediaType.APPLICATION_JSON})
335   @ApiOperation(
336           value = "Returns all user-related calendars",
337           notes = "This method lists all the calendars a specific user can see.")
338   @ApiResponses(value = {
339       @ApiResponse(code = 200, message = "Successful retrieval of all user-related calendars"),
340       @ApiResponse(code = 404, message = "Bad Request, or no calendars associated to the user"),
341       @ApiResponse(code = 503, message = "Can't generate JSON file") })
342   public Response getCalendars(
343         @ApiParam(value = "The calendar type to search for. It can be one of \"personal, group, shared\"", required = false, allowableValues = "personal, group, shared") @QueryParam("type") String type,
344         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
345         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
346         @ApiParam(value = "Tell the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
347         @ApiParam(value = "This is a list of comma-separated property's names of response json object", required = false) @QueryParam("fields") String fields,
348         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
349         @Context UriInfo uri) {
350     try {
351       limit = parseLimit(limit);
352       Type calType = Calendar.Type.UNDEFINED;
353 
354       if (type != null) {
355         try {
356           calType = Calendar.Type.valueOf(type.toUpperCase());
357         } catch (IllegalArgumentException ex) {
358           // Use default Type.UNDEFINED in any case of exception
359           log.debug(ex);
360         }
361       }
362 
363       CalendarCollection<Calendar> cals = calendarServiceInstance().getAllCalendars(currentUserId(), calType.type(), offset, limit);
364       if(cals == null || cals.isEmpty()) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
365 
366       String basePath = getBasePath(uri);
367       Collection data = new LinkedList();
368       Iterator<Calendar> calIterator = cals.iterator();
369       while (calIterator.hasNext()) {
370          Calendar cal = calIterator.next();
371          setCalType(cal);
372          data.add(extractObject(new CalendarResource(cal, basePath), fields));
373       }
374 
375       CollectionResource calData = new CollectionResource(data, returnSize ? cals.getFullSize() : -1);
376       calData.setOffset(offset);
377       calData.setLimit(limit);
378 
379       ResponseBuilder okResult;
380       if (jsonp != null) {
381         JsonValue value = new JsonGeneratorImpl().createJsonObject(calData);
382         StringBuilder sb = new StringBuilder(jsonp);
383         sb.append("(").append(value).append(");");
384         okResult = Response.ok(sb.toString(), new MediaType("text", "javascript"));
385       } else {
386         okResult = Response.ok(calData, MediaType.APPLICATION_JSON);
387       }
388 
389       if (returnSize) {
390         okResult.header(HEADER_LINK, buildFullUrl(uri, offset, limit, calData.getSize()));
391       }
392 
393       //
394       return okResult.cacheControl(nc).build();
395     } catch (Exception e) {
396       if(log.isDebugEnabled()) log.debug(e.getMessage());
397     }
398     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
399 
400   }
401 
402   /**
403    * Creates a calendar based on calendar attributes sent in the request body. Can be personal or group calendar.
404    *
405    * This accepts HTTP POST request, with JSON object (cal) represents a CalendarResource. Example:
406    *    {
407    *      name: "Calendar name",
408    *      description: "Description of the calendar"
409    *   }
410    *
411    * @param cal JSON object contains attributes of calendar object to create.
412    *        All attributes are optional. If specified explicitly, calendar name must not empty,
413    *        contains only letter, digit, space, "-", "_" characters. Default value of calendar name is: calendar.
414    *
415    * @request  POST: http://localhost:8080/rest/private/v1/calendar/calendars
416    *
417    * @response  HTTP status code:
418    *            201 if created successfully, and HTTP header *location* href that points to the newly created calendar.
419    *            401 if the user does not have create permission, 503 if any error during save process.
420    *
421    * @return  HTTP status code.
422    *
423    * @authentication
424    *
425    * @anchor  CalendarRestApi.createCalendar
426    */
427   @POST
428   @RolesAllowed("users")
429   @Path("/calendars/")
430   @ApiOperation(
431       value = "Creates a calendar",
432       notes = "Creates a calendar if: <br/>"
433           + "- this is a personal calendar and the user is authenticated<br/>"
434           + "- this is a group calendar and the user is authenticated and belongs to the group."
435       )
436   @ApiResponses(value = {
437       @ApiResponse(code = 201, message = "Calendar successfully created"),
438       @ApiResponse(code = 401, message = "The User isn't authorized to create a calendar there")
439       })
440   public Response createCalendar(CalendarResource cal, @Context UriInfo uriInfo) {
441     Calendar calendar = new Calendar();
442     if (cal.getName() == null) {
443       cal.setName(DEFAULT_CAL_NAME);
444     }
445     if (cal.getOwner() == null) {
446       cal.setOwner(currentUserId());
447     }
448     Response error = buildCalendar(calendar, cal);
449     if (error != null) {
450       return error;
451     }
452 
453     if (cal.getGroups() != null && cal.getGroups().length > 0) {
454       // Create a group calendar
455       if (isInGroups(cal.getGroups())) {
456         calendarServiceInstance().savePublicCalendar(calendar, true);
457       } else {
458         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
459       }
460     } else {
461       if (cal.getOwner() != null && !cal.getOwner().equals(currentUserId())) {
462         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
463       } else {
464         // Create a personal calendar
465             final String username = currentUserId();
466         calendarServiceInstance().saveUserCalendar(username, calendar, true);
467 
468             // Share calendar if user set edit or view permission
469             String[] viewPermissions = calendar.getViewPermission();
470             if (viewPermissions != null && viewPermissions.length > 0) {
471               Set<String> sharedUsers = new HashSet<String>();
472               Set<String> sharedGroups = new HashSet<String>();
473 
474               for (String permission : viewPermissions) {
475                 PermissionOwner perm = PermissionOwner.createPermissionOwnerFrom(permission);
476                 if (PermissionOwner.USER_OWNER.equals(perm.getOwnerType())) {
477                   sharedUsers.add(perm.getId());
478 
479                 } else if (PermissionOwner.GROUP_OWNER.equals(perm.getOwnerType())) {
480                   sharedGroups.add(perm.getGroupId());
481 
482                 } else if (PermissionOwner.MEMBERSHIP_OWNER.equals(perm.getOwnerType())) {
483                   try {
484                     sharedUsers.addAll(Utils.getUserByMembershipId(perm.getMembership(), perm.getGroupId()));
485 
486                   } catch (Exception ex) {
487                     log.warn("Can not share calendar to Membership: " + permission, ex);
488                   }
489                 }
490               }
491 
492               if (sharedGroups.size() > 0) {
493                 try {
494                   calendarServiceInstance().shareCalendarByRunJob(username, calendar.getId(), new ArrayList<String>(sharedGroups));
495                 } catch (Exception ex) {
496                   log.warn("Exception while share calendar to groups", ex);
497                 }
498               }
499 
500               if (sharedUsers.size() > 0) {
501                 try {
502                   calendarServiceInstance().shareCalendar(username, calendar.getId(), new ArrayList<String>(sharedUsers));
503                 } catch (Exception ex) {
504                   log.warn("Exception while share calendar to users", ex);
505                 }
506               }
507 
508             }
509       }
510     }
511     StringBuilder location = new StringBuilder(getBasePath(uriInfo));
512     location.append(CALENDAR_URI);
513     location.append(calendar.getId());
514     return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc).build();
515   }
516 
517   /**
518    * Search for a calendar by its id, in one of conditions:
519    * The authenticated user is the owner of the calendar,
520    * OR the user belongs to the group of the calendar,
521    * OR the calendar has been shared with the user or with a group of the user.
522    *
523    * @param id Identity of the calendar to be retrieved.
524    *
525    * @param fields Comma-separated list of selective calendar attributes to be returned. All returned if not specified.
526    *
527    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
528    *        If not specified, only JSON object is returned.
529    *
530    * @request  GET: http://localhost:8080/rest/private/v1/calendar/calendars/{id}
531    *
532    * @format  JSON
533    *
534    * @response
535    * {
536    *   "editPermission": "/platform/users/:*.*;",
537    *   "viewPermission": "*.*;",
538    *   "privateURL": null,
539    *   "publicURL": null,
540    *   "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/calendar8b8f65e77f00010122b425ec81b80da9/ics",
541    *   "description": null,
542    *   "color": "asparagus",
543    *   "timeZone": "Europe/Brussels",
544    *   "name": "Users",
545    *   "type": "2",
546    *   "owner": null,
547    *   "groups": [
548    *     "/platform/users"
549    *   ],
550    *   "href": "http://localhost:8080/rest/private/v1/calendar/calendars/calendar8b8f65e77f00010122b425ec81b80da9",
551    *   "id": "calendar8b8f65e77f00010122b425ec81b80da9"
552    * }
553    * @return  Calendar as JSON.
554    *
555    * @authentication
556    *
557    * @anchor  CalendarRestApi.getCalendarById
558    */
559   @GET
560   @RolesAllowed("users")
561   @Path("/calendars/{id}")
562   @Produces(MediaType.APPLICATION_JSON)
563   @ApiOperation(
564       value = "Finds a calendar by ID",
565       notes = "Returns the calendar with the specified id parameter if:<br/>"
566           + "- The authenticated user is the owner of the calendar<br/>"
567           + "- The authenticated user belongs to the group of the calendar<br/>"
568           + "- The calendar has been shared with the authenticated user or with a group of the authenticated user")
569   @ApiResponses(value = {
570       @ApiResponse(code = 200, message = "Successful retrieval of the calendar"),
571       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found"),
572           @ApiResponse(code = 503, message = "Can't generate JSON file")
573       })
574   public Response getCalendarById(
575       @ApiParam(value = "Identity of the calendar to retrieve", required = true) @PathParam("id") String id,
576       @ApiParam(value = "This is a list of comma-separated property's names of response json object", required = false) @QueryParam("fields") String fields,
577       @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
578       @Context UriInfo uriInfo,
579       @Context Request request) {
580     try {
581       CalendarService service = calendarServiceInstance();
582       Calendar cal = service.getCalendarById(id);
583       if(cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
584       cal.setCalType(service.getTypeOfCalendar(currentUserId(), cal.getId()));
585 
586       Date lastModified = new Date(cal.getLastModified());
587       ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
588       if (preCondition != null) {
589         return preCondition.build();
590       }
591 
592       CalendarResource calData = null;
593       if (this.hasViewCalendarPermission(cal, currentUserId())) {
594         setCalType(cal);
595         calData = new CalendarResource(cal, getBasePath(uriInfo));
596       }
597       if (calData == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
598 
599       Object resource = extractObject(calData, fields);
600       if (jsonp != null) {
601         String json = null;
602         if (resource instanceof Map) json = new JSONObject(resource).toString();
603         else {
604           JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
605           json = generatorImpl.createJsonObject(resource).toString();
606         }
607         StringBuilder sb = new StringBuilder(jsonp);
608         sb.append("(").append(json).append(");");
609         return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(cc).lastModified(lastModified).build();
610       }
611 
612       //
613       return Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(cc).lastModified(lastModified).build();
614     } catch (Exception e) {
615       if(log.isDebugEnabled()) log.debug(e.getMessage());
616     }
617     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
618   }
619 
620   /**
621    * Update a calendar specified by id, in one of conditions:
622    * the authenticated user is the owner of the calendar,
623    * OR for group calendars, the authenticated user has edit permission on the calendar.
624    *
625    * This accepts HTTP PUT request, with JSON object (calObj) in the request body, and calendar id in the path.
626    * All the attributes of JSON object are optional, any absent/invalid ones will be ignored.
627    * *id*, *href* and URLs attributes are Read-only.
628    *
629    * @param id Identity of the updated calendar.
630    *
631    * @param calObj JSON object contains attributes of calendar object to update, all the attributes are optional.
632    *
633    * @request  PUT: http://localhost:8080/rest/private/v1/calendar/calendars/demo-defaultCalendarId
634    *
635    * @response  HTTP status code: 200 if updated successfully, 404 if calendar not found,
636    *            401 if the user does not have edit permission, 403 if user submit invalid data to update
637    *            503 if any error during save process.
638    *
639    * @return HTTP status code.
640    *
641    * @authentication
642    *
643    * @anchor CalendarRestApi.updateCalendarById
644    */
645   @PUT
646   @RolesAllowed("users")
647   @Path("/calendars/{id}")
648   @ApiOperation(
649       value = "Updates a calendar",
650       notes = "Update the calendar with specified id if:<br/>"
651           + "- the authenticated user is the owner of the calendar<br/>"
652           + "- for group calendars, the authenticated user has edit rights on the calendar")
653   @ApiResponses(value = {
654       @ApiResponse(code = 200, message = "Calendar successfully updated"),
655       @ApiResponse(code = 401, message = "User unauthorized to update the calendar"),
656           @ApiResponse(code = 403, message = "If user try to update invalid data to the calendar"),
657       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found"),
658       @ApiResponse(code = 503, message = "An error occurred during the saving process")
659       })
660   public Response updateCalendarById(
661       @ApiParam(value = "Identity of the calendar to update", required = true) @PathParam("id") String id,
662       CalendarResource calObj) {
663     try {
664       Calendar cal = calendarServiceInstance().getCalendarById(id);
665       if(cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
666 
667       //Only allow to edit if user is owner of calendar, or have edit permission on group calendar
668       //don't allow to edit shared calendar, or remote calendar
669       if ((currentUserId().equals(cal.getCalendarOwner()) || cal.getGroups() != null) &&
670           Utils.isCalendarEditable(currentUserId(), cal)) {
671 
672         final List<String> oldViewPermissions;
673         if (cal.getViewPermission() != null) {
674           oldViewPermissions = Collections.unmodifiableList(Arrays.<String>asList(cal.getViewPermission()));
675         } else {
676           oldViewPermissions = Collections.<String>emptyList();
677         }
678 
679         Response error = buildCalendar(cal, calObj);
680         if (error != null) {
681           return error;
682         } else {
683           int type = calendarServiceInstance().getTypeOfCalendar(currentUserId(), cal.getId());
684 
685           if (type == Calendar.TYPE_PRIVATE) {
686             if (!currentUserId().equals(cal.getCalendarOwner())) {
687               return Response.status(HTTPStatus.FORBIDDEN).entity("Can not change owner of personal calendar").cacheControl(nc).build();
688             }
689             if (cal.getGroups() != null && cal.getGroups().length > 0) {
690               return Response.status(HTTPStatus.FORBIDDEN).entity("Can not update groups of personal calendar").cacheControl(nc).build();
691             }
692           }
693 
694           calendarServiceInstance().saveCalendar(cal.getCalendarOwner(), cal, type, false);
695 
696           if (type == Calendar.TYPE_PRIVATE) {
697             final List<String> viewPermissions;
698             if (cal.getViewPermission() != null) {
699               viewPermissions = Arrays.asList(cal.getViewPermission());
700             } else {
701               viewPermissions = Collections.emptyList();
702             }
703 
704             //. Only update when new viewPermission is different with old viewPermission
705             boolean needUpdateShare = false;
706             if (oldViewPermissions.size() != viewPermissions.size()) {
707               needUpdateShare = true;
708             } else {
709               for (String p : oldViewPermissions) {
710                 if (!viewPermissions.contains(p)) {
711                   needUpdateShare = true;
712                   break;
713                 }
714               }
715             }
716 
717             if (needUpdateShare) {
718               final String username = currentUserId();
719               final String calendarId = cal.getId();
720               Set<String> newSharedUsers = new HashSet<String>();
721               Set<String> newSharedGroups = new HashSet<String>();
722               for (String p : viewPermissions) {
723                 PermissionOwner perm = PermissionOwner.createPermissionOwnerFrom(p);
724                 String ownerType = perm.getOwnerType();
725                 if (PermissionOwner.USER_OWNER.equals(ownerType)) {
726                   newSharedUsers.add(perm.getId());
727                 } else if (PermissionOwner.GROUP_OWNER.equals(ownerType)) {
728                   newSharedGroups.add(perm.getGroupId());
729                 } else if (PermissionOwner.MEMBERSHIP_OWNER.equals(ownerType)) {
730                   try {
731                     newSharedUsers.addAll(Utils.getUserByMembershipId(perm.getMembership(), perm.getGroupId()));
732                   } catch (Exception ex) {
733                     log.warn("Exception while try to share calendar to Membership: " + p, ex);
734                   }
735                 }
736               }
737 
738               Set<String> removeShareUsers = new HashSet<String>();
739               Set<String> removeShareGroups = new HashSet<String>();
740               if (oldViewPermissions.size() > 0) {
741                 for (String p : oldViewPermissions) {
742                   if (viewPermissions.contains(p)) {
743                     continue;
744                   }
745 
746                   PermissionOwner perm = PermissionOwner.createPermissionOwnerFrom(p);
747                   String ownerType = perm.getOwnerType();
748                   if (PermissionOwner.USER_OWNER.equals(ownerType)) {
749                     removeShareUsers.add(perm.getId());
750                   } else if (PermissionOwner.GROUP_OWNER.equals(ownerType)) {
751                     removeShareGroups.add(perm.getGroupId());
752                   } else if (PermissionOwner.MEMBERSHIP_OWNER.equals(ownerType)) {
753                     try {
754                       removeShareUsers.addAll(Utils.getUserByMembershipId(perm.getMembership(), perm.getGroupId()));
755                     } catch (Exception ex) {
756                       log.error("Exception when try unshare calendar to Membership: " + p, ex);
757                     }
758                   }
759                 }
760               }
761 
762               // Remove all who shared before but not share any more
763               for (String user : removeShareUsers) {
764                 calendarServiceInstance().removeSharedCalendar(user, calendarId);
765               }
766               if (removeShareGroups.size() > 0) {
767                 calendarServiceInstance().removeSharedCalendarByJob(username,
768                                                        new ArrayList<String>(removeShareGroups), calendarId);
769               }
770 
771               // Share to new user or group
772               if (newSharedUsers.size() > 0) {
773                 newSharedUsers.remove(username);
774                 calendarServiceInstance().shareCalendar(username, calendarId, new ArrayList<String>(newSharedUsers));
775               }
776               if (newSharedGroups.size() > 0) {
777                 calendarServiceInstance().shareCalendarByRunJob(username,
778                                                         calendarId, new ArrayList<String>(newSharedGroups));
779               }
780             }
781           }
782 
783           return Response.ok().cacheControl(nc).build();
784         }
785       }
786 
787       //
788       return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
789     } catch (Exception e) {
790       log.error(e);
791     }
792     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
793   }
794 
795   /**
796    * Deletes a calendar specified by id, in one of conditions:
797    * - the authenticated user is the owner of the calendar.
798    * - for group calendars, the authenticated user has edit permission on the calendar.
799    * - if it is a shared calendar, the calendar is not shared anymore (but not deleted).
800    *
801    * @param id Identity of the calendar to be deleted.
802    *
803    * @request  DELETE: http://localhost:8080/rest/private/v1/calendar/calendars/demo-defaultCalendarId
804    *
805    * @response  HTTP status code: 200 if updated successfully, 404 if calendar not found,
806    *            401 if the user does not have permissions, 503 if any error during save process.
807    *
808    * @return HTTP status code.
809    *
810    * @authentication
811    *
812    * @anchor CalendarRestApi.deleteCalendarById
813    */
814   @DELETE
815   @RolesAllowed("users")
816   @Path("/calendars/{id}")
817   @ApiOperation(
818       value = "Deletes a calendar",
819       notes = "Delete the calendar with the specified id if:<br/>"
820           + "- the authenticated user is the owner of the calendar.<br/>"
821           + "- for group calendars, the authenticated user has edit rights on the calendar.<br/>"
822           + "- If it is a shared calendar the calendar is not shared anymore (but the original calendar is not deleted).")
823   @ApiResponses(value = {
824       @ApiResponse(code = 200, message = "Calendar successfully deleted"),
825       @ApiResponse(code = 401, message = "User unauthorized to delete the calendar"),
826       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found"),
827       @ApiResponse(code = 503, message = "An error occurred during the saving process")
828   })
829   public Response deleteCalendarById(
830       @ApiParam(value = "Identity of the calendar to delete", required = true) @PathParam("id") String id) {
831     try {
832       Calendar cal = calendarServiceInstance().getCalendarById(id);
833       if(cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
834 
835       cal.setCalType(calendarServiceInstance().getTypeOfCalendar(currentUserId(), id));
836       if (Utils.isCalendarEditable(currentUserId(), cal) || cal.getCalType() == Calendar.TYPE_SHARED) {
837         switch (cal.getCalType()) {
838         case Calendar.TYPE_PRIVATE:
839           calendarServiceInstance().removeUserCalendar(cal.getCalendarOwner(), id);
840           break;
841         case Calendar.TYPE_PUBLIC:
842           calendarServiceInstance().removePublicCalendar(id);
843           break;
844         case Calendar.TYPE_SHARED:
845           if (this.hasViewCalendarPermission(cal, currentUserId())) {
846             calendarServiceInstance().removeSharedCalendar(currentUserId(),id);
847             break;
848           }
849         }
850         return Response.ok().cacheControl(nc).build();
851       } else {
852         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
853       }
854     } catch (Exception e) {
855       if(log.isDebugEnabled()) log.debug(e.getMessage());
856     }
857     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
858   }
859 
860   /**
861    * Exports a calendar specified by id into iCal formated file, with one of conditions:
862    * The calendar is public,
863    * OR the authenticated user is the owner of the calendar,
864    * OR the user belongs to the group of the calendar,
865    * OR the calendar has been shared with the user or with a group of the user.
866    *
867    * @param id Identity of the exported calendar.
868    *
869    * @request GET: http://localhost:8080/rest/private/v1/calendar/calendars/demo-defaultCalendarId/ics
870    *
871    * @format text/calendar
872    *
873    * @response ICS file on success, or HTTP status code 404 on failure.
874    *
875    * @return  ICS file on success, or HTTP status code 404 on failure.
876    *
877    * @authentication
878    *
879    * @anchor CalendarRestApi.exportCalendarToIcs
880    */
881   @GET
882   @RolesAllowed("users")
883   @Path("/calendars/{id}/ics")
884   @Produces(TEXT_ICS)
885   @ApiOperation(
886       value = "Exports a calendar to iCal",
887       notes = "Returns an iCalendar formated file which is exported from the calendar with specified id if:<br/>"
888           + "- the calendar is public<br/>"
889           + "- the authenticated user is the owner of the calendar<br/>"
890           + "- the authenticated user belongs to the group of the calendar<br/>"
891           + "- the calendar has been shared with the authenticated user or with a group of the authenticated user")
892   @ApiResponses(value = {
893       @ApiResponse(code = 200, message = "Calendar successfully exported to ICS"),
894       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found"),
895       @ApiResponse(code = 503, message = "An error occurred")
896   })
897   public Response exportCalendarToIcs(
898       @ApiParam(value = "Identity of the calendar to retrieve ICS file", required = true) @PathParam("id") String id,
899       @Context Request request) {
900     try {
901       Calendar cal = calendarServiceInstance().getCalendarById(id);
902       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
903 
904       if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId())) {
905         int type = calendarServiceInstance().getTypeOfCalendar(currentUserId(),id);
906         String username = currentUserId();
907         if (type == -1) {
908           //this is a workaround
909           //calendarService can't find type of calendar correctly
910           type = Calendar.TYPE_PRIVATE;
911           username = cal.getCalendarOwner();
912         }
913 
914         CalendarImportExport iCalExport = calendarServiceInstance().getCalendarImportExports(CalendarService.ICALENDAR);
915         ArrayList<String> calIds = new ArrayList<String>();
916         calIds.add(id);
917         OutputStream out = iCalExport.exportCalendar(username, calIds, String.valueOf(type), Utils.UNLIMITED);
918 
919         // In case calendar hasn't got any event/task, CalendarImportExport service will return NULL
920         // But in this case, we still should return an ICS file without any event/task (like Google do)
921         // This is workaround to reach it without update in CalendarImportExport. (Because CalendarImportExport is used in some other business that need NULL value returned).
922         if (out == null) {
923           net.fortuna.ical4j.model.Calendar calendar = new net.fortuna.ical4j.model.Calendar();
924           calendar.getProperties().add(new ProdId("-//Ben Fortuna//iCal4j 1.0//EN"));
925           calendar.getProperties().add(Version.VERSION_2_0);
926           calendar.getProperties().add(CalScale.GREGORIAN);
927           calendar.getProperties().add(Method.REQUEST);
928           out = new ByteArrayOutputStream();
929           CalendarOutputter output = new CalendarOutputter(false);
930           output.output(calendar, out);
931         }
932 
933         byte[] data = out.toString().getBytes();
934 
935         byte[] hashCode = digest(data).getBytes();
936         EntityTag tag = new EntityTag(new String(hashCode));
937         ResponseBuilder preCondition = request.evaluatePreconditions(tag);
938         if (preCondition != null) {
939           return preCondition.build();
940         }
941 
942         InputStream in = new ByteArrayInputStream(data);
943         return Response.ok(in, TEXT_ICS_TYPE)
944             .header("Content-Disposition", "attachment;filename=\"" + cal.getName() + Utils.ICS_EXT)
945             .cacheControl(cc).tag(tag).build();
946       } else {
947         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
948       }
949     } catch (Exception e) {
950       if(log.isDebugEnabled()) log.debug(e.getMessage());
951     }
952     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
953 
954   }
955 
956   /**
957    * Searches for an event by id, in one of conditions:
958    * The calendar of the event is public,
959    * OR the authenticated user is the owner of the calendar,
960    * OR the user belongs to the group of the calendar,
961    * OR the user is a participant of the event,
962    * OR the calendar of the event has been shared with the user or with a group of the user.
963    *
964    * @param id Identity of the event.
965    *
966    * @param fields Comma-separated list of selective event attributes to be returned. All returned if not specified.
967    *
968    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
969    *        If not specified, only JSON object is returned.
970    *
971    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only.
972    *        This is a comma-separated list of property names. For example: expand=calendar,categories. In case of collections,
973    *        you can specify offset (default: 0), limit (default: *defaultLimit*). For example, expand=categories(1,5).
974    *        Instead of:
975    *        {
976    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
977    *        }
978    *        It returns:
979    *        {
980    *            "calendar": {
981    *              "editPermission": "",
982    *              "viewPermission": "",
983    *              "privateURL": null,
984    *              "publicURL": null,
985    *              "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
986    *              "description": null,
987    *              "color": "asparagus",
988    *              "timeZone": "Europe/Brussels",
989    *              "name": "John Smith",
990    *              "type": "0",
991    *              "owner": "john",
992    *              "groups": null,
993    *              "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
994    *              "id": "john-defaultCalendarId"
995    *            },
996    *        }
997    *
998    * @request GET: http://localhost:8080/rest/private/v1/calendar/events/Event123
999    *
1000    * @format JSON
1001    *
1002    * @response
1003    * {
1004    *   "to": "2015-07-24T01:30:00.000Z",
1005    *   "attachments": [],
1006    *   "from": "2015-07-24T01:00:00.000Z",
1007    *   "categories": [
1008    *     "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll"
1009    *   ],
1010    *   "categoryId": "defaultEventCategoryIdAll",
1011    *   "availability": "busy",
1012    *   "repeat": {},
1013    *   "reminder": [],
1014    *   "privacy": "private",
1015    *   "recurrenceId": null,
1016    *   "participants": [
1017    *     "john"
1018    *   ],
1019    *   "originalEvent": null,
1020    *   "description": null,
1021    *   "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1022    *   "subject": "event123",
1023    *   "location": null,
1024    *   "priority": "none",
1025    *   "href": "http://localhost:8080/rest/private/v1/calendar/events/Eventa9c5b87b7f00010178ce661a6beb020d",
1026    *   "id": "Eventa9c5b87b7f00010178ce661a6beb020d"
1027    * }
1028    *
1029    * @return  Event as JSON object.
1030    *
1031    * @authentication
1032    *
1033    * @anchor CalendarRestApi.getEventById
1034    */
1035   @GET
1036   @RolesAllowed("users")
1037   @Path("/events/{id}")
1038   @Produces(MediaType.APPLICATION_JSON)
1039   @ApiOperation(
1040       value = "Returns an event by ID",
1041       notes = "Returns an event with specified id parameter if:<br/>"
1042           + "- the calendar of the event is public<br/>"
1043           + "- the authenticated user is the owner of the calendar of the event<br/>"
1044           + "- the authenticated user belongs to the group of the calendar of the event<br/>"
1045           + "- the authenticated user is a participant of the event<br/>"
1046           + "- the calendar of the event has been shared with the authenticated user or with a group of the authenticated user"
1047       )
1048   @ApiResponses(value = {
1049       @ApiResponse(code = 200, message = "Successful retrieval of the event"),
1050       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
1051       @ApiResponse(code = 503, message = "An error occurred")
1052   })
1053   public Response getEventById(
1054       @ApiParam(value = "Identity of the event to find", required = true) @PathParam("id") String id,
1055       @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
1056       @ApiParam(value = "Used to ask for a full representation of a subresource, instead of only its link", required = false) @QueryParam("expand") String expand,
1057       @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
1058       @Context UriInfo uriInfo,
1059       @Context Request request) {
1060     try {
1061       CalendarService service = calendarServiceInstance();
1062       CalendarEvent ev = service.getEventById(id);
1063       if(ev == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1064 
1065       Date lastModified = new Date(ev.getLastModified());
1066       ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
1067       if (preCondition != null) {
1068         return preCondition.build();
1069       }
1070 
1071       Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
1072       boolean inParticipant = false;
1073       String[] participant = ev.getParticipant();
1074       if (participant != null) {
1075         Arrays.sort(participant);
1076         if (Arrays.binarySearch(participant, currentUserId()) > -1) inParticipant = true;
1077       }
1078 
1079       if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId()) || inParticipant) {
1080         Object resource = buildEventResource(ev, uriInfo, expand, fields);
1081         return buildJsonP(resource, jsonp).cacheControl(cc).lastModified(lastModified).build();
1082       } else {
1083         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1084       }
1085     } catch (Exception e) {
1086       if(log.isDebugEnabled()) log.debug(e.getMessage());
1087     }
1088     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
1089   }
1090 
1091   /**
1092    * Updates an event specified by id, in one of conditions:
1093    * The authenticated user is the owner of the calendar of the event,
1094    * OR for group calendars, the user has edit permission on the calendar,
1095    * OR the calendar has been shared with the user, with edit permission,
1096    * OR the calendar has been shared with a group of the user, with edit permission.
1097    *
1098    * This accepts HTTP PUT request, with JSON object (evObject) in the request body, and event id in the path.
1099    * All the attributes of JSON object are optional, any absent/invalid ones will be ignored.
1100    * Read-only attributes: *id* and *href*, *originalEvent*, *calendar*, *recurrentId* will be ignored too.
1101    *
1102    * @param id Identity of the updated event.
1103    *
1104    * @param evObject JSON object contains event attributes, all are optional.
1105    *        If provided explicitly (not null), attributes are checked with some rules:
1106    *        1. *subject* must not be empty.
1107    *        2. *availability* can only be one of "available", "busy", "outside".
1108    *        3. *repeat.repeatOn* can only be one of "MO", "TU", "WE", "TH", "FR", "SA", "SU".
1109    *        4. *repeat.repeatBy* {@literal must be >= 1 and <= 31}.
1110    *        5. *repeat.repeatType* must be one of "norepeat", "daily", "weekly", "monthly", "yearly".
1111    *        6. *from* date must be earlier than *to* date.
1112    *        7. *priority* must be one of "none", "high", "normal", "low".
1113    *        8. *privacy* can only be "public" or "private".
1114    *
1115    * @request PUT: http://localhost:8080/rest/private/v1/calendar/events/Event123
1116    *
1117    * @response  HTTP status code: 200 if updated successfully, 400 if parameters are not valid, 404 if event does not exist,
1118    *            401 if the user does not have edit permission, 503 if any error during save process.
1119    *
1120    * @return HTTP status code
1121    *
1122    * @authentication
1123    *
1124    * @anchor CalendarRestApi.updateEventById
1125    */
1126   @PUT
1127   @RolesAllowed("users")
1128   @Path("/events/{id}")
1129   @ApiOperation(
1130       value = "Updates an event identified by its ID",
1131       notes = "Updates the event with specified id if:<br/>"
1132           + "- the authenticated user is the owner of the calendar of the event<br/>"
1133           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
1134           + "- the calendar of the event has been shared with the authenticated user, with modification rights<br/>"
1135           + "- the calendar of the event has been shared with a group of the authenticated user, with modification rights")
1136   @ApiResponses(value = {
1137       @ApiResponse(code = 200, message = "Event successfully updated"),
1138       @ApiResponse(code = 400, message = "Bad Request, parameters not valid"),
1139       @ApiResponse(code = 401, message = "User unauthorized to update the event"),
1140       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
1141       @ApiResponse(code = 503, message = "Error during the saving process")
1142   })
1143   public Response updateEventById(
1144       @ApiParam(value = "Identity of the event to update", required = true) @PathParam("id") String id,
1145       @ApiParam(value = "Recurring update type, can be ALL, FOLLOWING or ONE, by default, it's ONE", required = false) @QueryParam("recurringUpdateType") RecurringUpdateType recurringUpdateType,
1146       EventResource evObject) {
1147     try {
1148       CalendarEvent event = calendarServiceInstance().getEventById(id);
1149       if(event == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1150       if (recurringUpdateType != null) {
1151         //clone the event to build occurrent event
1152         event = CalendarEvent.build(event);
1153       }
1154 
1155       Calendar moveToCal = null;
1156       if (evObject.getCalendarId() != null && !event.getCalendarId().equals(evObject.getCalendarId())) {
1157         moveToCal = calendarServiceInstance().getCalendarById(evObject.getCalendarId());
1158       }
1159 
1160       Calendar cal = calendarServiceInstance().getCalendarById(event.getCalendarId());
1161       int fromType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), cal.getId());
1162       if (Utils.isCalendarEditable(currentUserId(), cal) && (moveToCal == null || Utils.isCalendarEditable(currentUserId(), moveToCal))) {
1163         Response error = buildEvent(event, evObject, moveToCal);
1164         if (error != null) {
1165           return error;
1166         }
1167 
1168         int toType;
1169         if(moveToCal != null) {
1170           toType = moveToCal.getCalType();
1171         } else {
1172           toType = fromType;
1173         }
1174 
1175         moveToCal = moveToCal == null ? cal : moveToCal;
1176         saveEvent(cal.getId(), moveToCal.getId(), fromType, toType, event, recurringUpdateType, false);
1177 
1178         return Response.ok().cacheControl(nc).build();
1179       }
1180 
1181       //
1182       return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
1183     } catch (Exception e) {
1184       if(log.isDebugEnabled()) log.debug(e.getMessage());
1185     }
1186     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
1187   }
1188 
1189   /**
1190    * Deletes an event specified by id, in one of conditions:
1191    * The authenticated user is the owner of the calendar of the event,
1192    * OR for group calendars, the user has edit permission on the calendar,
1193    * OR the calendar has been shared with the user, with edit permission,
1194    * OR the calendar has been shared with a group of the user, with edit permission.
1195    *
1196    * @param id Identity of the event.
1197    *
1198    * @request DELETE: http://localhost:8080/rest/private/v1/calendar/events/Event123
1199    *
1200    * @response  HTTP status code: 200 if deleted successfully, 404 if event not found,
1201    *            401 if the user does not have edit permission, 503 if any error during save process.
1202    *
1203    * @return HTTP status code
1204    *
1205    * @authentication
1206    *
1207    * @anchor CalendarRestApi.deleteEventById
1208    */
1209   @DELETE
1210   @RolesAllowed("users")
1211   @Path("/events/{id}")
1212   @ApiOperation(
1213       value = "Deletes an event identified by its ID",
1214       notes = "Delete an event with specified id parameter if:<br/>"
1215           + "- the authenticated user is the owner of the calendar of the event<br/>"
1216           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
1217           + "- the calendar of the event has been shared with the authenticated user, with modification rights<br/>"
1218           + "- the calendar of the event has been shared with a group of the authenticated user, with modification rights")
1219   @ApiResponses(value = {
1220       @ApiResponse(code = 200, message = "Event deleted successfully"),
1221       @ApiResponse(code = 401, message = "User unauthorized to delete this event"),
1222       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
1223       @ApiResponse(code = 503, message = "An error occurred during the saving process")
1224   })
1225   public Response deleteEventById(
1226       @ApiParam(value = "identity of the event to delete", required = true) @PathParam("id") String id) {
1227     try {
1228       CalendarEvent ev = calendarServiceInstance().getEventById(id);
1229       if(ev == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1230 
1231       Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
1232       if (Utils.isCalendarEditable(currentUserId(), cal)) {
1233         int calType = Calendar.TYPE_ALL;
1234         try {
1235           calType = Integer.parseInt(ev.getCalType());
1236         } catch (NumberFormatException e) {
1237           calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), ev.getCalendarId());
1238         }
1239         switch (calType) {
1240         case Calendar.TYPE_PRIVATE:
1241           calendarServiceInstance().removeUserEvent(currentUserId(), ev.getCalendarId(), id);
1242           break;
1243         case Calendar.TYPE_PUBLIC:
1244           calendarServiceInstance().removePublicEvent(ev.getCalendarId(),id);
1245           break;
1246         case Calendar.TYPE_SHARED:
1247           calendarServiceInstance().removeSharedEvent(currentUserId(), ev.getCalendarId(), id);
1248           break;
1249 
1250         default:
1251           break;
1252         }
1253         return Response.ok().cacheControl(nc).build();
1254       } else {
1255         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
1256       }
1257     } catch (Exception e) {
1258       if(log.isDebugEnabled()) log.debug(e.getMessage());
1259     }
1260     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
1261   }
1262 
1263 
1264   /**
1265    * Returns attachments of an event specified by event id, in one of conditions:
1266    * The calendar of the event is public,
1267    * OR the authenticated user is the owner of the calendar,
1268    * OR the user belongs to the group of the calendar,
1269    * OR the user is a participant of the event,
1270    * OR the calendar has been shared with the user or with a group of the user.
1271    *
1272    * @param id Identity of the event.
1273    *
1274    * @param offset The starting point when paging the result. Default is *0*.
1275    *
1276    * @param limit Maximum number of attachments returned.
1277    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
1278    *
1279    * @param fields Comma-separated list of selective attachment attributes to be returned. All returned if not specified.
1280    *
1281    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
1282    *        If not specified, only JSON object is returned.
1283    *
1284    * @request GET: http://localhost:8080/rest/private/v1/calendar/events/Event123/attachments
1285    *
1286    * @format JSON
1287    *
1288    * @response
1289    * {
1290    *   "limit": 10,
1291    *   "data": [
1292    *     {
1293    *       "mimeType": "image/jpeg",
1294    *       "weight": 31249,
1295    *       "name": "test.jpg",
1296    *       "href": "...",
1297    *       "id": "..."
1298    *     }
1299    *   ],
1300    *   "size": 1,
1301    *   "offset": 0
1302    * }
1303    *
1304    * @return  Attachments in JSON, or HTTP status 404 if event not found.
1305    *
1306    * @authentication
1307    *
1308    * @anchor CalendarRestApi.getAttachmentsFromEvent
1309    */
1310   @SuppressWarnings({ "rawtypes", "unchecked" })
1311   @GET
1312   @RolesAllowed("users")
1313   @Path("/events/{id}/attachments")
1314   @Produces(MediaType.APPLICATION_JSON)
1315   @ApiOperation(
1316       value = "Returns the attachments of an event identified by its ID",
1317       notes = "Returns attachments of an event with specified id if:<br/>"
1318           + "- the calendar of the event is public<br/>"
1319           + "- the authenticated user is the owner of the calendar of the event<br/>"
1320           + "- the authenticated user belongs to the group of the calendar of the event<br/>"
1321           + "- the authenticated user is a participant of the event<br/>"
1322           + "- the calendar of the event has been shared with the authenticated user or with a group of the authenticated user")
1323   @ApiResponses(value = {
1324       @ApiResponse(code = 200, message = "Successful retrieval of all attachments"),
1325       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
1326       @ApiResponse(code = 503, message = "An error occured during the saving process")
1327   })
1328   public Response getAttachmentsFromEvent(
1329         @ApiParam(value = "Identity of an event to query for attachments", required = true) @PathParam("id") String id,
1330         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
1331         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used*", required = false) @QueryParam("limit") int limit,
1332         @ApiParam(value = "This is a list of comma-separated property's names of response json object", required = false) @QueryParam("fields") String fields,
1333         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
1334         @Context UriInfo uriInfo) {
1335     try {
1336       limit = parseLimit(limit);
1337 
1338       CalendarEvent ev = calendarServiceInstance().getEventById(id);
1339       if(ev == null || ev.getAttachment() == null) {
1340         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1341       } else {
1342         Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
1343         boolean inParticipant = false;
1344         if (ev.getParticipant() != null) {
1345           String[] participant = ev.getParticipant();
1346           Arrays.sort(participant);
1347           int i = Arrays.binarySearch(participant, currentUserId());
1348           if (i > -1) inParticipant = true;
1349         }
1350 
1351         if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId()) || inParticipant) {
1352           Iterator<Attachment> it = ev.getAttachment().iterator();
1353           List attResource = new ArrayList();
1354           Utils.skip(it, offset);
1355           int counter = 0;
1356           String basePath = getBasePath(uriInfo);
1357           while (it.hasNext()) {
1358             Attachment a = it.next();
1359             attResource.add(extractObject(new AttachmentResource(a, basePath), fields));
1360             if(++counter == limit) break;
1361           }
1362           CollectionResource evData = new CollectionResource(attResource, ev.getAttachment().size());
1363           evData.setOffset(offset);
1364           evData.setLimit(limit);
1365 
1366           if (jsonp != null) {
1367             JsonValue value = new JsonGeneratorImpl().createJsonObject(evData);
1368             StringBuilder sb = new StringBuilder(jsonp);
1369             sb.append("(").append(value).append(");");
1370             return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(nc).header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, evData.getSize())).build();
1371           }
1372 
1373           //
1374           return Response.ok(evData, MediaType.APPLICATION_JSON).header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, evData.getSize())).cacheControl(nc).build();
1375         }
1376 
1377         //
1378         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1379       }
1380     } catch (Exception e) {
1381       if(log.isDebugEnabled()) log.debug(e.getMessage());
1382     }
1383     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
1384   }
1385 
1386   /**
1387    * Creates attachments for an event specified by id, in one of conditions:
1388    * The authenticated user is the owner of the calendar of the event,
1389    * OR for group calendars, the user has edit permission on the calendar,
1390    * OR the calendar has been shared with the user, with edit permission,
1391    * OR the calendar has been shared with a group of the user, with edit permission.
1392    *
1393    * This accepts HTTP POST request, with files in HTTP form submit request, and the event id in path.
1394    *
1395    * @param id Identity of the event.
1396    *
1397    * @param iter Iterator of org.apache.commons.fileupload.FileItem objects.
1398    *        (eXo Rest framework uses Apache file upload to parse the input stream of HTTP form submit request, and inject FileItem objects).
1399    *
1400    * @request POST: http://localhost:8080/rest/private/v1/calendar/events/Event123/attachments
1401    *
1402    * @response  HTTP status code:
1403    *            201 if created successfully, and HTTP header *location* href that points to the newly created attachments.
1404    *            404 if event not found, 401 if the user does not have create permission, 503 if any error during save process.
1405    *
1406    * @return HTTP status code
1407    *
1408    * @authentication
1409    *
1410    * @anchor CalendarRestApi.createAttachmentForEvent
1411    */
1412   @POST
1413   @RolesAllowed("users")
1414   @Path("/events/{id}/attachments")
1415   @Consumes("multipart/*")
1416   @ApiOperation(
1417       value = "Creates attachments for an event identified by its ID",
1418       notes = "Creates attachments for an event with specified id if:<br/>"
1419           + "- the authenticated user is the owner of the calendar of the event<br/>"
1420           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
1421           + "- the calendar of the event has been shared with the authenticated user, with modification rights<br/>"
1422           + "- the calendar of the event has been shared with a group of the authenticated user, with modification rights")
1423   @ApiResponses(value = {
1424       @ApiResponse(code = 201, message = "Attachment successfully created"),
1425       @ApiResponse(code = 401, message = "User unauthorized to create an attachment to this event"),
1426       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
1427       @ApiResponse(code = 503, message = "An error occurred during the saving process")
1428   })
1429   public Response createAttachmentForEvent(
1430       @Context UriInfo uriInfo,
1431       @ApiParam(value = "Identity of an event where the attachment is created", required = true) @PathParam("id") String id,
1432       Iterator<FileItem> iter) {
1433     try {
1434       CalendarEvent event = calendarServiceInstance().getEventById(id);
1435       if (event == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1436 
1437       Calendar cal = calendarServiceInstance().getCalendarById(event.getCalendarId());
1438 
1439       if (Utils.isCalendarEditable(currentUserId(), cal)) {
1440         int calType = Calendar.TYPE_ALL;
1441         List<Attachment> attachment = new ArrayList<Attachment>();
1442         try {
1443           calType = Integer.parseInt(event.getCalType());
1444         } catch (NumberFormatException e) {
1445           calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), event.getCalendarId());
1446         }
1447 
1448         attachment.addAll(event.getAttachment());
1449         while (iter.hasNext()) {
1450           FileItem file  = iter.next();
1451           String fileName = file.getName();
1452           if(fileName != null) {
1453             String mimeType = new MimeTypeResolver().getMimeType(fileName.toLowerCase());
1454             Attachment at = new Attachment();
1455             at.setMimeType(mimeType);
1456             at.setSize(file.getSize());
1457             at.setName(file.getName());
1458             at.setInputStream(file.getInputStream());
1459             attachment.add(at);
1460           }
1461         }
1462         event.setAttachment(attachment);
1463 
1464         saveEvent(calType, event, false);
1465 
1466         StringBuilder attUri = new StringBuilder(getBasePath(uriInfo));
1467         attUri.append("/").append(event.getId());
1468         attUri.append(ATTACHMENT_URI);
1469         return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, attUri.toString()).cacheControl(nc).build();
1470       }
1471 
1472       //
1473       return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
1474     } catch (Exception e) {
1475       if(log.isDebugEnabled()) log.debug(e.getMessage());
1476     }
1477     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
1478   }
1479 
1480   /**
1481    * Returns events of a calendar specified by id, in one of conditions:
1482    * The calendar is public,
1483    * OR the authenticated user is the owner of the calendar,
1484    * OR the user belongs to the group of the calendar,
1485    * OR the user is a participant of the event,
1486    * OR the calendar has been shared with the user or with a group of the user.
1487    *
1488    * @param id Identity of a calendar to search for events.
1489    *
1490    * @param start Date that complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.
1491    *        Default: current server time.
1492    *
1493    * @param end Date that complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date.
1494    *        Default: current server time + 1 week.
1495    *
1496    * @param category Search for this category only. If not specified, search events of all categories.
1497    *
1498    * @param offset The starting point when paging the result. Default is *0*.
1499    *
1500    * @param limit Maximum number of events returned.
1501    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
1502    *
1503    * @param returnSize Default is *false*. If set to *true*, the total number of matched calendars will be returned in JSON,
1504    *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
1505    *
1506    * @param fields Comma-separated list of selective event properties to be returned. All returned if not specified.
1507    *
1508    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
1509    *        If not specified, only JSON object is returned.
1510    *
1511    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only.
1512    *        This is a comma-separated list of event attribute names. For example: expand=calendar,categories. In case of collections,
1513    *        you can specify offset (default: 0), limit (default: *defaultLimit*). For example, expand=categories(1,5).
1514    *        Instead of:
1515    *        {
1516    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1517    *        }
1518    *        It returns:
1519    *        {
1520    *            "calendar": {
1521    *            "editPermission": "",
1522    *            "viewPermission": "",
1523    *            "privateURL": null,
1524    *            "publicURL": null,
1525    *            "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
1526    *            "description": null,
1527    *            "color": "asparagus",
1528    *            "timeZone": "Europe/Brussels",
1529    *            "name": "John Smith",
1530    *            "type": "0",
1531    *            "owner": "john",
1532    *            "groups": null,
1533    *            "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1534    *            "id": "john-defaultCalendarId"
1535    *            },
1536    *        }
1537    *
1538    * @request  {@code GET: http://localhost:8080/rest/private/v1/calendar/calendars/myCalId/events?category=meeting&expand=calendar,categories(1,5)}
1539    *
1540    * @format JSON
1541    *
1542    * @response
1543    * {
1544    *   "limit": 10,
1545    *   "data": [
1546    *     {
1547    *       "to": "2015-07-24T01:30:00.000Z",
1548    *       "attachments": [],
1549    *       "from": "2015-07-24T01:00:00.000Z",
1550    *       "categories": [
1551    *         "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll"
1552    *       ],
1553    *       "categoryId": "defaultEventCategoryIdAll",
1554    *       "availability": "busy",
1555    *       "repeat": {},
1556    *       "reminder": [],
1557    *       "privacy": "private",
1558    *       "recurrenceId": null,
1559    *       "participants": [
1560    *         "john"
1561    *       ],
1562    *       "originalEvent": null,
1563    *       "description": null,
1564    *       "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1565    *       "subject": "event123",
1566    *       "location": null,
1567    *       "priority": "none",
1568    *       "href": "http://localhost:8080/rest/private/v1/calendar/events/Eventa9c5b87b7f00010178ce661a6beb020d",
1569    *       "id": "Eventa9c5b87b7f00010178ce661a6beb020d"
1570    *     }
1571    *   ],
1572    *   "size": -1,
1573    *   "offset": 0
1574    * }
1575    *
1576    * @return  List of events in JSON, or HTTP status 404 if the calendar is not found.
1577    *
1578    * @authentication
1579    *
1580    * @anchor CalendarRestApi.getEventsByCalendar
1581    */
1582   @SuppressWarnings({ "rawtypes", "unchecked" })
1583   @GET
1584   @RolesAllowed("users")
1585   @Path("/calendars/{id}/events")
1586   @Produces(MediaType.APPLICATION_JSON)
1587   @ApiOperation(
1588       value = "Returns the events of a calendar identified by its ID",
1589       notes = "Returns events of an calendar with specified id when:<br/>"
1590           + "- the calendar is public<br/>"
1591           + "- the authenticated user is the owner of the calendar of the event<br/>"
1592           + "- the authenticated user belongs to the group of the calendar of the event<br/>"
1593           + "- the authenticated user is a participant of the event<br/>"
1594           + "- the calendar of the event has been shared with the authenticated user or with a group of the authenticated user")
1595   @ApiResponses(value = {
1596       @ApiResponse(code = 200, message = "Successful retrieval of all events from the calendar"),
1597       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found")
1598   })
1599   public Response getEventsByCalendar(
1600         @ApiParam(value = "Identity of a calendar to search for events", required = true) @PathParam("id") String id,
1601         @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date", required = false, defaultValue = "Current server time") @QueryParam("startTime") String start,
1602         @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date", required = false, defaultValue = "Current server time + 1 week") @QueryParam("endTime") String end,
1603         @ApiParam(value = "Search for this category only. If not specified, search event of any category", required = false) @QueryParam("category") String category,
1604         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
1605         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
1606         @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
1607         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
1608         @ApiParam(value = "Used to ask for a full representation of a subresource, instead of only its link", required = false) @QueryParam("expand") String expand,
1609         @ApiParam(value = "Tells the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
1610         @Context UriInfo uri) throws Exception {
1611     limit = parseLimit(limit);
1612     String username = currentUserId();
1613 
1614     CalendarService service = calendarServiceInstance();
1615     EventDAO evtDAO = service.getEventDAO();
1616 
1617     long fullSize = returnSize ? 0 : -1;
1618     List data = new LinkedList();
1619     Calendar calendar = service.getCalendarById(id);
1620 
1621     if (calendar != null) {
1622       if (calendar.hasChildren()) {
1623         String participant = null;
1624         if (calendar.getPublicUrl() == null && !hasViewCalendarPermission(calendar, username)) {
1625           participant = username;
1626         }
1627 
1628         EventQuery eventQuery = buildEventQuery(start, end, category, Arrays.asList(calendar),
1629                                                 id, participant, CalendarEvent.TYPE_EVENT);
1630         ListAccess<CalendarEvent> events = evtDAO.findEventsByQuery(eventQuery);
1631 
1632         //
1633         for (CalendarEvent event : events.load(offset, limit)) {
1634           data.add(buildEventResource(event, uri, expand, fields));
1635         }
1636         if (returnSize) {
1637           fullSize = events.getSize();
1638         }
1639       }
1640     } else {
1641       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1642     }
1643     //
1644     CollectionResource evData = new CollectionResource(data, fullSize);
1645     evData.setOffset(offset);
1646     evData.setLimit(limit);
1647 
1648     ResponseBuilder response = buildJsonP(evData, jsonp);
1649 
1650     if (returnSize) {
1651       response.header(HEADER_LINK, buildFullUrl(uri, offset, limit, fullSize));
1652     }
1653 
1654     //
1655     return response.build();
1656   }
1657 
1658   /**
1659    * Returns events of the authenticated user.
1660    *
1661    * @param start Date that complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.
1662    *        Default: current server time.
1663    *
1664    * @param end Date that complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date.
1665    *        Default: current server time + 1 week.
1666    *
1667    * @param category Search for this category only. If not specified, search events of all categories.
1668    *
1669    * @param offset The starting point when paging the result. Default is *0*.
1670    *
1671    * @param limit Maximum number of events returned.
1672    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
1673    *
1674    * @param returnSize Default is *false*. If set to *true*, the total number of matched calendars will be returned in JSON,
1675    *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
1676    *
1677    * @param fields Comma-separated list of selective event properties to be returned. All returned if not specified.
1678    *
1679    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
1680    *        If not specified, only JSON object is returned.
1681    *
1682    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only.
1683    *        This is a comma-separated list of event attribute names. For example: expand=calendar,categories. In case of collections,
1684    *        you can specify offset (default: 0), limit (default: *query_limit*). For example, expand=categories(1,5).
1685    *        Instead of:
1686    *        {
1687    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1688    *        }
1689    *        It returns:
1690    *        {
1691    *            "calendar": {
1692    *            "editPermission": "",
1693    *            "viewPermission": "",
1694    *            "privateURL": null,
1695    *            "publicURL": null,
1696    *            "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
1697    *            "description": null,
1698    *            "color": "asparagus",
1699    *            "timeZone": "Europe/Brussels",
1700    *            "name": "John Smith",
1701    *            "type": "0",
1702    *            "owner": "john",
1703    *            "groups": null,
1704    *            "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1705    *            "id": "john-defaultCalendarId"
1706    *            },
1707    *        }
1708    *
1709    * @request  {@code GET: http://localhost:8080/rest/private/v1/calendar/calendars/myCalId/events?category=meeting&expand=calendar,categories(1,5)}
1710    *
1711    * @format JSON
1712    *
1713    * @response
1714    * {
1715    *   "limit": 10,
1716    *   "data": [
1717    *     {
1718    *       "to": "2015-07-24T01:30:00.000Z",
1719    *       "attachments": [],
1720    *       "from": "2015-07-24T01:00:00.000Z",
1721    *       "categories": [
1722    *         "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll"
1723    *       ],
1724    *       "categoryId": "defaultEventCategoryIdAll",
1725    *       "availability": "busy",
1726    *       "repeat": {},
1727    *       "reminder": [],
1728    *       "privacy": "private",
1729    *       "recurrenceId": null,
1730    *       "participants": [
1731    *         "john"
1732    *       ],
1733    *       "originalEvent": null,
1734    *       "description": null,
1735    *       "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1736    *       "subject": "event123",
1737    *       "location": null,
1738    *       "priority": "none",
1739    *       "href": "http://localhost:8080/rest/private/v1/calendar/events/Eventa9c5b87b7f00010178ce661a6beb020d",
1740    *       "id": "Eventa9c5b87b7f00010178ce661a6beb020d"
1741    *     }
1742    *   ],
1743    *   "size": -1,
1744    *   "offset": 0
1745    * }
1746    *
1747    * @return  List of events in JSON, or HTTP status 404 if the calendar is not found.
1748    *
1749    * @authentication
1750    *
1751    * @anchor CalendarRestApi.getEventsByCalendar
1752    */
1753   @SuppressWarnings({ "rawtypes", "unchecked" })
1754   @GET
1755   @RolesAllowed("users")
1756   @Path("/events")
1757   @Produces(MediaType.APPLICATION_JSON)
1758   @ApiOperation(
1759           value = "Returns the events of a calendar identified by its ID",
1760           notes = "Returns events of an calendar with specified id when:<br/>"
1761                   + "- the calendar is public<br/>"
1762                   + "- the authenticated user is the owner of the calendar of the event<br/>"
1763                   + "- the authenticated user belongs to the group of the calendar of the event<br/>"
1764                   + "- the authenticated user is a participant of the event<br/>"
1765                   + "- the calendar of the event has been shared with the authenticated user or with a group of the authenticated user")
1766   @ApiResponses(value = {
1767           @ApiResponse(code = 200, message = "Successful retrieval of all events from the calendar"),
1768           @ApiResponse(code = 404, message = "Calendar with provided ID Not Found")
1769   })
1770   public Response getEvents(
1771           @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date", required = false, defaultValue = "Current server time") @QueryParam("startTime") String start,
1772           @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date", required = false, defaultValue = "Current server time + 1 week") @QueryParam("endTime") String end,
1773           @ApiParam(value = "Search for this category only. If not specified, search event of any category", required = false) @QueryParam("category") String category,
1774           @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
1775           @ApiParam(value = "The maximum number of results when paging through a list of entities. If not specified or exceed the *query_limit* configuration of calendar rest service, it will use the *query_limit*", required = false) @QueryParam("limit") int limit,
1776           @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
1777           @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
1778           @ApiParam(value = "Used to ask for a full representation of a subresource, instead of only its link", required = false) @QueryParam("expand") String expand,
1779           @ApiParam(value = "Tells the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
1780           @Context UriInfo uri) throws Exception {
1781     limit = parseLimit(limit);
1782     String username = currentUserId();
1783 
1784     CalendarService service = calendarServiceInstance();
1785     EventDAO evtDAO = service.getEventDAO();
1786 
1787     long fullSize = returnSize ? 0 : -1;
1788     List data = new LinkedList();
1789     List<Calendar> calendarList;
1790     try {
1791       calendarList = getCalendarsOfUser(service, username);
1792     } catch (Exception e) {
1793       log.error("Cannot find calendars of user " + username, e);
1794       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1795     }
1796 
1797     EventQuery eventQuery = buildEventQuery(start, end, category, calendarList,
1798             null, username, CalendarEvent.TYPE_EVENT);
1799     ListAccess<CalendarEvent> events = evtDAO.findEventsByQuery(eventQuery);
1800 
1801     //
1802     for (CalendarEvent event : events.load(offset, limit)) {
1803       data.add(buildEventResource(event, uri, expand, fields));
1804     }
1805     if (returnSize) {
1806       fullSize = events.getSize();
1807     }
1808     CollectionResource evData = new CollectionResource(data, fullSize);
1809     evData.setOffset(offset);
1810     evData.setLimit(limit);
1811 
1812     ResponseBuilder response = buildJsonP(evData, jsonp);
1813 
1814     if (returnSize) {
1815       response.header(HEADER_LINK, buildFullUrl(uri, offset, limit, fullSize));
1816     }
1817 
1818     //
1819     return response.build();
1820   }
1821 
1822   private List<Calendar> getCalendarsOfUser(CalendarService service, String username) throws Exception {
1823     List<Calendar> list = new ArrayList<>();
1824     List<GroupCalendarData> listgroupCalendar = service.getGroupCalendars(getUserGroups(username), true, username);
1825     for (GroupCalendarData group : listgroupCalendar) {
1826       Optional.ofNullable(group.getCalendars()).ifPresent(list::addAll);
1827     }
1828     Optional.ofNullable(service.getUserCalendars(username, true)).ifPresent(list::addAll);
1829     return list;
1830   }
1831 
1832   private String[] getUserGroups(String username) throws Exception {
1833     String [] groupsList;
1834     Object[] objs = orgService.getGroupHandler().findGroupsOfUser(username).toArray();
1835     groupsList = new String[objs.length];
1836     for (int i = 0; i < objs.length; i++) {
1837       groupsList[i] = ((Group) objs[i]).getId();
1838     }
1839     return groupsList;
1840   }
1841 
1842   /**
1843    * Creates an event in a calendar specified by id, in one of conditions:
1844    * The authenticated user is the owner of the calendar,
1845    * OR for group calendars, the user has edit permission on the calendar,
1846    * OR the calendar has been shared with the user, with edit permission,
1847    * OR the calendar has been shared with a group of the user, with edit permission.
1848    *
1849    * This accepts HTTP POST request, with JSON object (evObject) in the request body.
1850    *
1851    * @param evObject JSON object contains attributes of event.
1852    *        All attributes are optional. If provided explicitly (not null), attributes are checked with some rules:
1853    *        1. *subject* must not be empty, default value is: default.
1854    *        2. *availability* can only be one of "available", "busy", "outside".
1855    *        3. *repeat.repeatOn* can only be one of "MO", "TU", "WE", "TH", "FR", "SA", "SU".
1856    *        4. *repeat.repeatBy* {@literal must be >= 1 and <= 31}.
1857    *        5. *repeat.repeatType* must be one of "norepeat", "daily", "weekly", "monthly", "yearly".
1858    *        6. *from* date must be earlier than *to* date.
1859    *        7. *priority* must be one of "none", "high", "normal", "low".
1860    *        8. *privacy* can only be public or private.
1861    *
1862    * @param id Identity of the calendar.
1863    *
1864    * @request  POST: http://localhost:8080/rest/private/v1/calendar/calendars/myCalId/events
1865    *
1866    * @response  HTTP status code:
1867    *            201 if created successfully, and HTTP header *location* href that points to the newly created event.
1868    *            400 if provided attributes are not valid (not pass the rule of evObject).
1869    *            404 if calendar not found.
1870    *            401 if the user does not have create permission.
1871    *            503 if any error during save process.
1872    *
1873    * @return  HTTP status code.
1874    *
1875    * @authentication
1876    *
1877    * @anchor CalendarRestApi.createEventForCalendar
1878    */
1879   @POST
1880   @RolesAllowed("users")
1881   @Path("/calendars/{id}/events")
1882   @ApiOperation(
1883       value = "Creates an event in a Calendar identified by its ID",
1884       notes = "Creates an event in a calendar with specified id only if:<br/>"
1885           + "- the authenticated user is the owner of the calendar<br/>"
1886           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
1887           + "- the calendar has been shared with the authenticated user, with modification rights<br/>"
1888           + "- the calendar has been shared with a group of the authenticated user, with modification rights")
1889   @ApiResponses(value = {
1890       @ApiResponse(code = 201, message = "Event successfully created in the Calendar"),
1891       @ApiResponse(code = 400, message = "Bad Request: Provided attributes are not valid (not following the rules of evObject)"),
1892       @ApiResponse(code = 401, message = "User unauthorized to create an event in this calendar"),
1893       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found"),
1894       @ApiResponse(code = 503, message = "An error occurred during the saving process")
1895   })
1896   public Response createEventForCalendar(
1897       @ApiParam(value = "Identity of the calendar where the event is created", required = true) @PathParam("id") String id,
1898       EventResource evObject,
1899       @Context UriInfo uriInfo) {
1900     try {
1901       Calendar cal = calendarServiceInstance().getCalendarById(id);
1902       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
1903       CalendarEvent newEvent = new CalendarEvent();
1904       if (evObject.getSubject() == null) {
1905         evObject.setSubject(DEFAULT_EVENT_NAME);
1906       }
1907       if (evObject.getCategoryId() == null) {
1908         evObject.setCategoryId(CalendarService.DEFAULT_EVENTCATEGORY_ID_ALL);
1909       }
1910       Response error = buildEvent(newEvent, evObject, null);
1911       if (error != null) {
1912         return error;
1913       }
1914       if (Utils.isCalendarEditable(currentUserId(), cal)) {
1915         int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), id);
1916 
1917         newEvent.setCalendarId(id);
1918         saveEvent(calType, newEvent, true);
1919         String username = ConversationState.getCurrent().getIdentity().getUserId();
1920         MailNotification mail = new MailNotification(mailService, orgService, calendarServiceInstance());
1921         mail.sendEmail(newEvent,username);
1922 
1923         String location = new StringBuilder(getBasePath(uriInfo)).append(EVENT_URI).append(newEvent.getId()).toString();
1924         return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc).build();
1925       } else {
1926         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
1927       }
1928     } catch (Exception e) {
1929       if(log.isDebugEnabled()) log.debug(e.getMessage());
1930     }
1931     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
1932 
1933   }
1934 
1935   /**
1936    * Returns occurrences of a recurring event specified by id, in one of conditions:
1937    * the calendar of the event is public,
1938    * OR the authenticated user is the owner of the calendar,
1939    * OR the user belongs to the group of the calendar,
1940    * OR the user is a participant of the event,
1941    * OR the calendar has been shared with the user or with a group of the user.
1942    * 
1943    * @param id Identity of the event.
1944    * 
1945    * @param start Date complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for occurrences *from* this date.
1946    *        Default: current server time.
1947    * 
1948    * @param end Date complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for occurrences *to* this date.
1949    *        Default: current server time + 1 week.
1950    *
1951    * @param offset The starting point when paging the result. Default is *0*.
1952    * 
1953    * @param limit Maximum number of occurrences returned.
1954    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
1955    * 
1956    * @param returnSize Default is *false*. If set to *true*, the total number of matched calendars will be returned in JSON,
1957    *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
1958    * 
1959    * @param fields Comma-separated list of selective attributes to be returned. All returned if not specified.
1960    * 
1961    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
1962    *        If not specified, only JSON object is returned.
1963    * 
1964    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only.
1965    *        This is a comma-separated list of property names. For example: expand=calendar,categories. In case of collections, 
1966    *        you can specify offset (default: 0), limit (default: *defaultLimit*). For example, expand=categories(1,5).
1967    *        Instead of: 
1968    *        {
1969    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1970    *        }
1971    *        It returns:
1972    *        {
1973    *            "calendar": {
1974    *              "editPermission": "",
1975    *              "viewPermission": "",
1976    *              "privateURL": null,
1977    *              "publicURL": null,
1978    *              "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
1979    *              "description": null,
1980    *              "color": "asparagus",
1981    *              "timeZone": "Europe/Brussels",
1982    *              "name": "John Smith",
1983    *              "type": "0",
1984    *              "owner": "john",
1985    *              "groups": null,
1986    *              "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
1987    *              "id": "john-defaultCalendarId"
1988    *            },
1989    *        }
1990    * 
1991    * @request  {@code GET: http://localhost:8080/rest/private/v1/calendar/events/Event123/occurences?offset=1&limit=5}
1992    * 
1993    * @format  JSON
1994    * 
1995    * @response 
1996    * {
1997    *   "limit": 0,
1998    *   "data": [
1999    *     {
2000    *       "to": "2015-07-24T02:30:00.000Z",
2001    *       "attachments": [],
2002    *       "from": "2015-07-24T02:00:00.000Z",
2003    *       "categories": [
2004    *         "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll"
2005    *       ],
2006    *       "categoryId": "defaultEventCategoryIdAll",
2007    *       "availability": "busy",
2008    *       "repeat": {},
2009    *       "reminder": [],
2010    *       "privacy": "private",
2011    *       "recurrenceId": null,
2012    *       "participants": [],
2013    *       "originalEvent": null,
2014    *       "description": null,
2015    *       "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2016    *       "subject": "rep123",
2017    *       "location": null,
2018    *       "priority": "none",
2019    *       "href": "http://localhost:8080/rest/private/v1/calendar/events/Eventaa3c68ee7f00010171ac205a54bc1419",
2020    *       "id": "Eventaa3c68ee7f00010171ac205a54bc1419"
2021    *     },
2022    *     {}
2023    *   ],
2024    *   "size": -1,
2025    *   "offset": 10
2026    * }
2027    * 
2028    * @return        List of occurrences.
2029    * 
2030    * @authentication
2031    *
2032    * @anchor CalendarRestApi.getOccurrencesFromEvent
2033    */
2034   @SuppressWarnings({ "unchecked", "rawtypes" })
2035   @GET
2036   @RolesAllowed("users")
2037   @Path("/events/{id}/occurrences")
2038   @Produces(MediaType.APPLICATION_JSON)
2039   @ApiOperation(
2040       value = "Returns occurrences of a recurring event identified by its ID",
2041       notes = "Returns occurrences of a recurring event with specified id when :<br/>"
2042           + "- the calendar of the event is public<br/>"
2043           + "- the authenticated user is the owner of the calendar of the event<br/>"
2044           + "- the authenticated user belongs to the group of the calendar of the event<br/>"
2045           + "- the authenticated user is a participant of the event<br/>"
2046           + "- the calendar of the event has been shared with the authenticated user or with a group of the authenticated user")
2047   @ApiResponses(value = {
2048       @ApiResponse(code = 200, message = "Successful retrieval of all occurrences of the event"),
2049       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
2050       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2051   })
2052   public Response getOccurrencesFromEvent(
2053           @ApiParam(value = "Identity of the recurrent event", required = true) @PathParam("id") String id,
2054           @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
2055           @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
2056           @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.", required = false, defaultValue = "current server time") @QueryParam("start") String start,
2057           @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date.", required = false, defaultValue = "current server time + 1 week") @QueryParam("end") String end,
2058           @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
2059           @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
2060           @ApiParam(value = "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", required = false) @QueryParam("expand") String expand,
2061           @ApiParam(value = "Tells the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
2062           @Context UriInfo uriInfo) {
2063     try {
2064       limit = parseLimit(limit);
2065       java.util.Calendar[] dates = parseDate(start, end);
2066 
2067       CalendarEvent recurEvent = calendarServiceInstance().getEventById(id);
2068       if (recurEvent == null)  return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2069       TimeZone tz = java.util.Calendar.getInstance().getTimeZone();
2070       String timeZone = tz.getID();
2071 
2072       Map<String,CalendarEvent> occMap = calendarServiceInstance().getOccurrenceEvents(recurEvent, dates[0], dates[1], timeZone);
2073       if(occMap == null || occMap.isEmpty()) {
2074         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2075       }
2076 
2077       Calendar cal = calendarServiceInstance().getCalendarById(recurEvent.getCalendarId());
2078       boolean inParticipant = false;
2079       if (recurEvent.getParticipant() != null) {
2080         String[] participant = recurEvent.getParticipant();
2081         Arrays.sort(participant);
2082         int i = Arrays.binarySearch(participant, currentUserId());
2083         if (i > -1) inParticipant = true;
2084       }
2085 
2086       if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId()) || inParticipant) {
2087         Collection data = new ArrayList();
2088         Iterator<CalendarEvent> evIter = occMap.values().iterator();
2089         Utils.skip(evIter, offset);
2090 
2091         int counter =0;
2092         while (evIter.hasNext()) {
2093           data.add(buildEventResource(evIter.next(), uriInfo, expand, fields));
2094           if(++counter == limit) break;
2095         }
2096 
2097         int fullSize = returnSize ? occMap.values().size() : -1;
2098         CollectionResource evData = new CollectionResource(data, fullSize);
2099         evData.setOffset(offset);
2100         evData.setLimit(limit);
2101         
2102         //
2103         ResponseBuilder response = buildJsonP(evData, jsonp);
2104         if (returnSize) {
2105           response.header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, evData.getSize()));
2106         }
2107         return response.build();
2108       }
2109 
2110       //
2111       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2112     } catch (Exception e) {
2113       if(log.isDebugEnabled()) log.debug(e.getMessage());
2114     }
2115     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2116   }
2117 
2118   /**
2119    * Returns tasks of a calendar specified by id, in one of conditions:
2120    * The calendar is public,
2121    * OR the authenticated user is the owner of the calendar,
2122    * OR the user belongs to the group of the calendar,
2123    * OR the task is delegated to the user,
2124    * OR the calendar has been shared with the user or with a group of the user.
2125    * 
2126    * @param id Identity of the calendar.
2127    * 
2128    * @param start Date complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for tasks *from* this date.
2129    *        Default: current server time.
2130    * 
2131    * @param end Date complies ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for tasks *to* this date.
2132    *        Default: current server time + 1 week.
2133    * 
2134    * @param category Filter the tasks by this category if specified.
2135    *
2136    * @param offset The starting point when paging the result. Default is *0*.
2137    * 
2138    * @param limit Maximum number of tasks returned.
2139    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
2140    * 
2141    * @param returnSize Default is *false*. If set to *true*, the total number of matched calendars will be returned in JSON,
2142    *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
2143    * 
2144    * @param fields Comma-separated list of selective task attributes to be returned. All returned if not specified.
2145    * 
2146    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
2147    *        If not specified, only JSON object is returned.
2148    * 
2149    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only. 
2150    *        This is a comma-separated list of property names. For example: expand=calendar,categories. In case of collections, 
2151    *        you can specify offset (default: 0), limit (default: *defaultLimit*). For example, expand=categories(1,5).
2152    *        Instead of: 
2153    *        {
2154    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2155    *        }
2156    *        It returns:
2157    *        {
2158    *            "calendar": {
2159    *              "editPermission": "",
2160    *              "viewPermission": "",
2161    *              "privateURL": null,
2162    *              "publicURL": null,
2163    *              "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
2164    *              "description": null,
2165    *              "color": "asparagus",
2166    *              "timeZone": "Europe/Brussels",
2167    *              "name": "John Smith",
2168    *              "type": "0",
2169    *              "owner": "john",
2170    *              "groups": null,
2171    *              "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2172    *              "id": "john-defaultCalendarId"
2173    *            },
2174    *        }
2175    * 
2176    * @request  GET: {@code http://localhost:8080/rest/private/v1/calendar/myCalId/tasks?category=meeting&expand=calendar,categories(1,5)}
2177    * 
2178    * @format JSON
2179    * 
2180    * @response 
2181    * {
2182    *   "limit": 10,
2183    *   "data": [
2184    *     {
2185    *       "to": "2015-07-18T12:00:00.000Z",
2186    *       "attachments": [],
2187    *       "from": "2015-07-18T11:30:00.000Z",
2188    *       "categories": [
2189    *         "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll"
2190    *       ],
2191    *       "categoryId": "defaultEventCategoryIdAll",
2192    *       "reminder": [],
2193    *       "delegation": [
2194    *         "john"
2195    *       ],
2196    *       "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2197    *       "name": "caramazov",
2198    *       "priority": "none",
2199    *       "note": null,
2200    *       "status": "",
2201    *       "href": "http://localhost:8080/rest/private/v1/calendar/tasks/Event99f63db07f0001016dd7a4b4e0e7125c",
2202    *       "id": "Event99f63db07f0001016dd7a4b4e0e7125c"
2203    *     }
2204    *   ],
2205    *   "size": -1,
2206    *   "offset": 0
2207    * }
2208    * 
2209    * @return  List of tasks in JSON, or HTTP status 404 if calendar not found.
2210    * 
2211    * @authentication
2212    *
2213    * @anchor CalendarRestApi.getTasksByCalendar
2214    */
2215   @SuppressWarnings({ "rawtypes", "unchecked" })
2216   @GET
2217   @RolesAllowed("users")
2218   @Path("/calendars/{id}/tasks")
2219   @Produces(MediaType.APPLICATION_JSON)
2220   @ApiOperation(
2221       value = "Returns tasks of a calendar identified by its ID",
2222       notes = "Returns tasks of a calendar with specified id when:<br/>"
2223           + "- the calendar is public<br/>"
2224           + "- the authenticated user is the owner of the calendar of the task<br/>"
2225           + "- the authenticated user belongs to the group of the calendar of the task<br/>"
2226           + "- the authenticated user is delegated by the task<br/>"
2227           + "- the calendar of the task has been shared with the authenticated user or with a group of the authenticated user")
2228   @ApiResponses(value = {
2229       @ApiResponse(code = 200, message = "Successful retrieval of all tasks from the calendar"),
2230       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found")
2231   })
2232   public Response getTasksByCalendar(
2233         @ApiParam(value = "Identity of a calendar to search for tasks", required = true) @PathParam("id") String id,
2234         @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *from* this date.", required = false, defaultValue = "current server time")@QueryParam("startTime") String start,
2235         @ApiParam(value = "Date follow ISO8601 (YYYY-MM-DDThh:mm:ssTZD). Search for events *to* this date.", required = false, defaultValue = "current server time + 1 week") @QueryParam("endTime") String end,
2236         @ApiParam(value = "Search for this category only", required = false, defaultValue = "If not specified, search task of any category") @QueryParam("category") String category,
2237         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
2238         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
2239         @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
2240         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
2241         @ApiParam(value = "used to ask for a full representation of a subresource, instead of only its link", required = false) @QueryParam("expand") String expand,
2242         @ApiParam(value = "Tells the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
2243         @Context UriInfo uri) throws Exception {
2244     limit = parseLimit(limit);
2245     String username = currentUserId();
2246 
2247     CalendarService service = calendarServiceInstance();
2248     EventDAO evtDAO = service.getEventDAO();
2249 
2250     long fullSize = returnSize ? 0 : -1;
2251     List data = new LinkedList();
2252     Calendar calendar = service.getCalendarById(id);
2253 
2254     if (calendar != null) {
2255       String participant = null;
2256       if (calendar.getPublicUrl() == null && !hasViewCalendarPermission(calendar, username)) {
2257         participant = username;
2258       }
2259 
2260       EventQuery eventQuery = buildEventQuery(start, end, category, Arrays.asList(calendar),
2261                                               id, participant, CalendarEvent.TYPE_TASK);
2262       ListAccess<CalendarEvent> events = evtDAO.findEventsByQuery(eventQuery);
2263 
2264       //
2265       for (CalendarEvent event : events.load(offset, limit)) {
2266         data.add(buildTaskResource(event, uri, expand, fields));
2267       }
2268       if (returnSize) {
2269         fullSize = events.getSize();
2270       }
2271     } else {
2272       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2273     }
2274     //
2275     CollectionResource evData = new CollectionResource(data, fullSize);
2276     evData.setOffset(offset);
2277     evData.setLimit(limit);
2278 
2279     ResponseBuilder response = buildJsonP(evData, jsonp);
2280 
2281     if (returnSize) {
2282       response.header(HEADER_LINK, buildFullUrl(uri, offset, limit, fullSize));
2283     }
2284 
2285     //
2286     return response.build();
2287   }
2288 
2289   /**
2290    * Creates a task for a calendar specified by id, in one of conditions:
2291    * The user is the owner of the calendar,
2292    * OR for group calendars, the user has edit permission on the calendar,
2293    * OR the calendar has been shared with the user, with edit permission,
2294    * OR the calendar has been shared with a group of the user, with edit permission.
2295    * 
2296    * This accepts HTTP POST request, with JSON object (evObject) in the request body. Example:
2297    *   {
2298    *      "name": "...", "note": "...",
2299    *      "categoryId": "",
2300    *      "from": "...", "to": "...",
2301    *      "delegation": ["...", ""], "priority": "", 
2302    *      "reminder": [],
2303    *      "status": ""
2304    *   }
2305    * 
2306    * @param evObject JSON object contains attributes of task.
2307    *        All attributes are optional. If provided explicitly (not null), attributes are checked with some rules:
2308    *        1. *name* must not be empty, default value is: "default".
2309    *        2. *from* date must be earlier than *to* date.
2310    *        3. *priority* must be one of "none", "high", "normal", "low".
2311    *        4. *status* must be one of "needs-action", "completed", "in-progress", "canceled".
2312    * 
2313    * @param id Identity of the calendar.
2314    * 
2315    * @request  POST: http://localhost:8080/rest/private/v1/calendar/calendars/myCalId/tasks
2316    * 
2317    * @response  HTTP status code: 
2318    *            201 if created successfully, and HTTP header *location* href that points to the newly created task.
2319    *            400 if attributes are invalid (not pass the rule of evObject).
2320    *            404 if calendar not found.
2321    *            401 if the user does not have create permission.
2322    *            503 if any error during save process.
2323    * 
2324    * @return  HTTP status code.
2325    * 
2326    * @authentication
2327    *
2328    * @anchor CalendarRestApi.createTaskForCalendar
2329    */
2330   @POST
2331   @RolesAllowed("users")
2332   @Path("/calendars/{id}/tasks")
2333   @ApiOperation(
2334       value = "Creates a task for a calendar identified by its ID",
2335       notes = "Creates a task for a calendar with specified id only if:<br/>"
2336           + "- the authenticated user is the owner of the calendar<br/>"
2337           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
2338           + "- the calendar has been shared with the authenticated user, with modification rights<br/>"
2339           + "- the calendar has been shared with a group of the authenticated user, with modification rights<br/>")
2340   @ApiResponses(value = {
2341       @ApiResponse(code = 201, message = "Task successfully created"),
2342       @ApiResponse(code = 400, message = "Bad Request: Provided attributes are not valid (not following the rules of evObject)"),
2343       @ApiResponse(code = 401, message = "User unauthorized to create a task for this calendar"),
2344       @ApiResponse(code = 404, message = "Calendar with provided ID Not Found"),
2345       @ApiResponse(code = 503, message = "An error occurred during saving process")
2346   })
2347   public Response createTaskForCalendar(
2348       @ApiParam(value = "Identity of the calendar where the task is created", required = true) @PathParam("id") String id,
2349       TaskResource evObject,
2350       @Context UriInfo uriInfo) {
2351     try {
2352       Calendar cal = calendarServiceInstance().getCalendarById(id);
2353       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2354 
2355       CalendarEvent newEvent = new CalendarEvent();
2356       newEvent.setEventType(CalendarEvent.TYPE_TASK);
2357       if (evObject.getName() == null) {
2358         evObject.setName(DEFAULT_EVENT_NAME);
2359       }
2360       if (evObject.getCategoryId() == null) {
2361         evObject.setCategoryId(CalendarService.DEFAULT_EVENTCATEGORY_ID_ALL);
2362       }
2363       Response error = buildEventFromTask(newEvent, evObject);
2364       if (error != null) {
2365         return error;
2366       }
2367       if (Utils.isCalendarEditable(currentUserId(), cal)) {
2368         int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), id);
2369 
2370         newEvent.setCalendarId(id);
2371         saveEvent(calType, newEvent, true);
2372 
2373         String location = new StringBuilder(getBasePath(uriInfo)).append(TASK_URI).append(newEvent.getId()).toString();
2374         return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc).build();
2375       } else {
2376         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
2377       }
2378     } catch (Exception e) {
2379       if(log.isDebugEnabled()) log.debug(e.getMessage());
2380     }
2381     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2382 
2383   }
2384 
2385   /**
2386    *
2387    * Returns a task specified by id, in one of conditions:
2388    * the calendar of the task is public;
2389    * OR the authenticated user is the owner of the calendar;
2390    * OR the user belongs to the group of the calendar;
2391    * OR the task is delegated to the user;
2392    * OR the calendar has been shared with the user or with a group of the user.
2393    * 
2394    * @param id Identity of the task.
2395    * 
2396    * @param fields Comma-separated list of selective task properties to be returned. All returned if not specified.
2397    * 
2398    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
2399    *        If not specified, only JSON object is returned.
2400    * 
2401    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only. 
2402    *        This is a comma-separated list of task attributes names. For example: expand=calendar,categories. In case of collections, 
2403    *        you can specify offset (default: 0), limit (default: *defaultLimit*). For example, expand=categories(1,5).
2404    *        Instead of: 
2405    *        {
2406    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2407    *        }
2408    *        It returns:
2409    *        {
2410    *            "calendar": {
2411    *              "editPermission": "",
2412    *              "viewPermission": "",
2413    *              "privateURL": null,
2414    *              "publicURL": null,
2415    *              "icsURL": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId/ics",
2416    *              "description": null,
2417    *              "color": "asparagus",
2418    *              "timeZone": "Europe/Brussels",
2419    *              "name": "John Smith",
2420    *              "type": "0",
2421    *              "owner": "john",
2422    *              "groups": null,
2423    *              "href": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2424    *              "id": "john-defaultCalendarId"
2425    *            },
2426    *        }
2427    * 
2428    * @request  GET: http://localhost:8080/rest/private/v1/calendar/tasks/Task123?fields=id,name
2429    * 
2430    * @format JSON
2431    * 
2432    * @response 
2433    * {
2434    *   "to": "2015-07-18T12:00:00.000Z",
2435    *   "attachments": [],
2436    *   "from": "2015-07-18T11:30:00.000Z",
2437    *   "categories": [
2438    *     "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll"
2439    *   ],
2440    *   "categoryId": "defaultEventCategoryIdAll",
2441    *   "reminder": [],
2442    *   "delegation": [
2443    *     "john"
2444    *   ],
2445    *   "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId",
2446    *   "name": "caramazov",
2447    *   "priority": "none",
2448    *   "note": null,
2449    *   "status": "",
2450    *   "href": "http://localhost:8080/rest/private/v1/calendar/tasks/Event99f63db07f0001016dd7a4b4e0e7125c",
2451    *   "id": "Event99f63db07f0001016dd7a4b4e0e7125c"
2452    * } 
2453    *  
2454    * @return  Task in JSON format, or HTTP status 404 if task not found, or 503 if any other failure.
2455    * 
2456    * @authentication
2457    *
2458    * @anchor CalendarRestApi.getTaskById
2459    */
2460   @GET
2461   @RolesAllowed("users")
2462   @Path("/tasks/{id}")
2463   @Produces(MediaType.APPLICATION_JSON)
2464   @ApiOperation(
2465       value = "Returns a task identified by its ID",
2466       notes = "Returns a task with specified id if:<br/>"
2467           + "- the calendar of the task is public<br/>"
2468           + "- the authenticated user is the owner of the calendar of the task<br/>"
2469           + "- the authenticated user belongs to the group of the calendar of the task<br/>"
2470           + "- the authenticated user is a participant of the task<br/>"
2471           + "- the calendar of the task has been shared with the authenticated user or with a group of the authenticated user"
2472       )
2473   @ApiResponses(value = {
2474       @ApiResponse(code = 200, message = "Successful retrieval of the task"),
2475       @ApiResponse(code = 404, message = "Task with provided ID Not Found"),
2476       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2477   })
2478   public Response getTaskById(
2479       @ApiParam(value = "Identity of the task to find", required = true) @PathParam("id") String id,
2480       @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
2481       @ApiParam(value = "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", required = false) @QueryParam("expand") String expand,
2482       @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
2483       @Context UriInfo uriInfo,
2484       @Context Request request) {
2485     try {
2486       CalendarEvent ev = calendarServiceInstance().getEventById(id);
2487       if(ev == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2488 
2489       Date lastModified = new Date(ev.getLastModified());
2490       ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
2491       if (preCondition != null) {
2492         return preCondition.build();
2493       }
2494 
2495       Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
2496       boolean inParticipant = false;
2497       if (ev.getParticipant() != null) {
2498         String[] participant = ev.getParticipant();
2499         Arrays.sort(participant);
2500         if (Arrays.binarySearch(participant, currentUserId()) > -1) inParticipant = true;;
2501       }
2502 
2503       if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId()) || inParticipant) {
2504         Object resource = buildTaskResource(ev, uriInfo, expand, fields);
2505         return buildJsonP(resource, jsonp).cacheControl(cc).lastModified(lastModified).build();
2506       } else {
2507         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2508       }
2509     } catch (Exception e) {
2510       if(log.isDebugEnabled()) log.debug(e.getMessage());
2511     }
2512     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2513   }
2514 
2515   /**
2516    *
2517    * Updates a task specified by id, in one of conditions:
2518    * the calendar of the task is public,
2519    * OR the authenticated user is the owner of the calendar,
2520    * OR the user belongs to the group of the calendar,
2521    * OR the task is delegated to the user,
2522    * OR the calendar has been shared with the user or with a group of the user.
2523    * 
2524    * This accepts HTTP PUT request, with JSON object (evObject) in the request body, and task id in the path.
2525    * All the attributes are optional, any absent/invalid attributes will be ignored.
2526    * *id*, *href*, *calendar* are Read-only.
2527    * For example:
2528    *   {
2529    *      "name": "...", "note": "...",
2530    *      "categoryId": "",
2531    *      "from": "...", "to": "...",
2532    *      "delegation": ["...", ""], "priority": "", 
2533    *      "reminder": [],
2534    *      "status": ""
2535    *   }
2536    *  
2537    * @param id Identity of the task.
2538    * 
2539    * @param evObject JSON object contains attributes of the task to be updated, all attributes are optional.
2540    *        If provided explicitly (not null), attributes are checked with some rules:
2541    *        1. *name* must not be empty.
2542    *        2. *from* date must be earlier than *to* date.
2543    *        3. *priority* must be one of "none", "high", "normal", "low".
2544    *        4. *status* must be one of "needs-action", "completed", "in-progress", "canceled".
2545    * 
2546    * @request  PUT: http://localhost:8080/rest/private/v1/calendar/tasks/Task123
2547    * 
2548    * @response  HTTP status code:
2549    *            200 if updated successfully,
2550    *            404 if task not found,
2551    *            400 if attributes are invalid, 
2552    *            401 if the user does not have edit permission,
2553    *            503 if any error during save process.
2554    * 
2555    * @return HTTP status code.
2556    * 
2557    * @authentication
2558    *
2559    * @anchor CalendarRestApi.updateTaskById
2560    */
2561   @PUT
2562   @RolesAllowed("users")
2563   @Path("/tasks/{id}")
2564   @ApiOperation(
2565       value = "Updates a task identified by its ID",
2566       notes = "Updates a task with the specified id if:<br/>"
2567           + "- the authenticated user is the owner of the calendar of the event<br/>"
2568           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
2569           + "- the calendar of the event has been shared with the authenticated user, with modification rights<br/>"
2570           + "- the calendar of the event has been shared with a group of the authenticated user, with modification rights")
2571   @ApiResponses(value = {
2572       @ApiResponse(code = 200, message = "Task successfully updated"),
2573       @ApiResponse(code = 400, message = "Bad Request: Provided attributes are not valid (not following the rules of evObject)"),
2574       @ApiResponse(code = 401, message = "User unauthorized to update this task"),
2575       @ApiResponse(code = 404, message = "Task with provided ID Not Found"),
2576       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2577   })
2578   public Response updateTaskById(
2579       @ApiParam(value = "Identity of the task to update", required = true) @PathParam("id") String id,
2580       TaskResource evObject) {
2581     try {
2582       CalendarEvent event = calendarServiceInstance().getEventById(id);
2583       if (event == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2584       Calendar cal = calendarServiceInstance().getCalendarById(event.getCalendarId());
2585       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2586 
2587       if (Utils.isCalendarEditable(currentUserId(), cal)) {
2588         int calType = -1;
2589         try {
2590           calType = Integer.parseInt(event.getCalType());
2591         }catch (NumberFormatException e) {
2592           calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), event.getCalendarId());
2593         }
2594         buildEventFromTask(event, evObject);
2595 
2596         saveEvent(calType, event, false);
2597 
2598         return Response.ok().cacheControl(nc).build();
2599       } else {
2600         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
2601       }
2602     } catch (Exception e) {
2603       if(log.isDebugEnabled()) log.debug(e.getMessage());
2604     }
2605     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2606   }
2607 
2608   /**
2609    * Deletes a task specified by id, in one of conditions:
2610    * the calendar of the task is public;
2611    * OR the authenticated user is the owner of the calendar;
2612    * OR the user belongs to the group of the calendar;
2613    * OR the task is delegated to the user;
2614    * OR the calendar has been shared with the user or with a group of the user.
2615    *  
2616    * @param id Identity of the task.
2617    * 
2618    * @request  DELETE: http://localhost:8080/rest/private/v1/calendar/tasks/Task123
2619    * 
2620    * @response  HTTP status code:
2621    *            200 if deleted successfully, 404 if task not found,
2622    *            401 if the user does not have permission, 503 if any error during save process.
2623    * 
2624    * @return HTTP status code.
2625    * 
2626    * @authentication
2627    *
2628    * @anchor CalendarRestApi.deleteTaskById
2629    */
2630   @DELETE
2631   @RolesAllowed("users")
2632   @Path("/tasks/{id}")
2633   @ApiOperation(
2634       value = "Deletes a task identified by its ID",
2635       notes = "Deletes a task with specified id if:<br/>"
2636           + "- the authenticated user is the owner of the calendar of the event<br/>"
2637           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
2638           + "- the calendar of the event has been shared with the authenticated user, with modification rights<br/>"
2639           + "- the calendar of the event has been shared with a group of the authenticated user, with modification rights")
2640   @ApiResponses(value = {
2641       @ApiResponse(code = 200, message = "Task successfully deleted"),
2642       @ApiResponse(code = 401, message = "User unauthorized to delete this task"),
2643       @ApiResponse(code = 404, message = "Task with provided ID Not Found"),
2644       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2645   })
2646   public Response deleteTaskById(
2647       @ApiParam(value = "Identity of the task to delete", required = true) @PathParam("id") String id) {
2648     try {
2649       CalendarEvent ev = calendarServiceInstance().getEventById(id);
2650       if (ev == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2651       Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
2652       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2653 
2654       if (Utils.isCalendarEditable(currentUserId(), cal)) {
2655         int calType = Calendar.TYPE_ALL;
2656         try {
2657           calType = Integer.parseInt(ev.getCalType());
2658         } catch (NumberFormatException e) {
2659           calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), ev.getCalendarId());
2660         }
2661         switch (calType) {
2662         case Calendar.TYPE_PRIVATE:
2663           calendarServiceInstance().removeUserEvent(currentUserId(), ev.getCalendarId(), id);
2664           break;
2665         case Calendar.TYPE_PUBLIC:
2666           calendarServiceInstance().removePublicEvent(ev.getCalendarId(),id);
2667           break;
2668         case Calendar.TYPE_SHARED:
2669           calendarServiceInstance().removeSharedEvent(currentUserId(), ev.getCalendarId(), id);
2670           break;
2671 
2672         default:
2673           break;
2674         }
2675         return Response.ok().cacheControl(nc).build();
2676       } else {
2677         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
2678       }
2679     } catch (Exception e) {
2680       if(log.isDebugEnabled()) log.debug(e.getMessage());
2681     }
2682     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2683   }
2684 
2685   /**
2686    * Queries an attachment (of an event/task) by attachment id, in one of conditions:
2687    * The calendar of the event/task is public,
2688    * OR the authenticated user is the owner of the calendar,
2689    * OR the user belongs to the group of the calendar,
2690    * OR the user is a participant of the event or is delegated to the task,
2691    * OR the calendar has been shared with the user or with a group of the user.
2692    * 
2693    * @param id Identity of the attachment.
2694    * 
2695    * @param fields Comma-separated list of selective attachment attributes to be returned. All returned if not specified.
2696    * 
2697    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
2698    *        If not specified, only JSON object is returned.
2699    * 
2700    * @request  GET: http://localhost:8080/rest/private/v1/calendar/attachments/att123?fields=id,name
2701    * 
2702    * @format JSON
2703    * 
2704    * @response 
2705    * {
2706    *   "weight": 38569,
2707    *   "mimeType": "image/png",
2708    *   "name": "test.png",
2709    *   "href": "...",
2710    *   "id": "..."
2711    * }
2712    *  
2713    * @return  Attachment info in JSON.
2714    * 
2715    * @authentication
2716    *
2717    * @anchor CalendarRestApi.getAttachmentById
2718    */
2719   @GET
2720   @RolesAllowed("users")
2721   @Path("/attachments/{id}")
2722   @Produces(MediaType.APPLICATION_JSON)
2723   @ApiOperation(
2724       value = "Returns an attachment identified by its ID",
2725       notes = "Returns an attachment with specified id if:<br/>"
2726           + "- the calendar of the event is public<br/>"
2727           + "- the authenticated user is the owner of the calendar of the event<br/>"
2728           + "- the authenticated user belongs to the group of the calendar of the event<br/>"
2729           + "- the authenticated user is a participant of the event<br/>"
2730           + "- the calendar of the event has been shared with the authenticated user or with a group of the authenticated user")
2731   @ApiResponses(value = {
2732       @ApiResponse(code = 200, message = "Successful retrieval of the attachment"),
2733       @ApiResponse(code = 404, message = "Attachment with provided ID Not Found"),
2734       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2735   })
2736   public Response getAttachmentById(
2737           @ApiParam(value = "Identity of the attachment to find", required = true) @PathParam("id") String id,
2738           @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
2739           @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
2740           @Context UriInfo uriInfo,
2741           @Context Request request) {
2742     try {
2743       id = AttachmentResource.decode(id);
2744       CalendarEvent ev = this.findEventAttachment(id);
2745       if (ev == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2746       Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
2747       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2748       Attachment att = calendarServiceInstance().getAttachmentById(id);
2749       if(att == null)  return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2750 
2751       Date lastModified = new Date(att.getLastModified());
2752       ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
2753       if (preCondition != null) {
2754         return preCondition.build();
2755       }
2756 
2757       boolean inParticipant = false;
2758       if (ev.getParticipant() != null) {
2759         String[] participant = ev.getParticipant();
2760         Arrays.sort(participant);
2761         int i = Arrays.binarySearch(participant, currentUserId());
2762         if (i > -1) inParticipant = true;
2763       }
2764 
2765       if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, currentUserId()) || inParticipant) {
2766 
2767         AttachmentResource evData = new AttachmentResource(att, getBasePath(uriInfo));
2768         Object resource = extractObject(evData, fields);
2769         if (jsonp != null) {
2770           String json = null;
2771           if (resource instanceof Map) json = new JSONObject(resource).toString();
2772           else {
2773             JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
2774             json = generatorImpl.createJsonObject(resource).toString();
2775           }
2776           StringBuilder sb = new StringBuilder(jsonp);
2777           sb.append("(").append(json).append(");");
2778           return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(cc).lastModified(lastModified).build();
2779         }
2780 
2781         //
2782         return Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(cc).lastModified(lastModified).build();
2783       }
2784 
2785       //
2786       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2787     } catch (Exception e) {
2788       if(log.isDebugEnabled()) log.debug(e.getMessage());
2789     }
2790     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2791   }
2792 
2793   /**
2794    * Deletes an attachment (of an event/task) specified by attachment id, in one of conditions:
2795    * The calendar of the event/task is public,
2796    * OR the authenticated user is the owner of the calendar,
2797    * OR the user belongs to the group of the calendar,
2798    * OR the user is a participant of the event or is delegated to the task,
2799    * OR the calendar has been shared with the user or with a group of the user.
2800    * 
2801    * @param id Identity of the attachment.
2802    * 
2803    * @request  DELETE: http://localhost:8080/rest/private/v1/calendar/attachments/att123
2804    * 
2805    * @response  HTTP status code:
2806    *            200 if deleted successfully, 404 if attachment not found,
2807    *            401 if the user does not have permission, 503 if any error during save process.
2808    * 
2809    * @return HTTP status code.
2810    * 
2811    * @authentication
2812    *
2813    * @anchor CalendarRestApi.deleteAttachmentById
2814    */
2815   @DELETE
2816   @RolesAllowed("users")
2817   @Path("/attachments/{id}")
2818   @ApiOperation(
2819       value = "Deletes an attachment identified by its ID",
2820       notes = "Deletes an attachment with specified id if:<br/>"
2821           + "- the authenticated user is the owner of the calendar of the event<br/>"
2822           + "- for group calendars, the authenticated user has edit rights on the calendar<br/>"
2823           + "- the calendar of the event has been shared with the authenticated user, with modification rights<br/>"
2824           + "- the calendar of the event has been shared with a group of the authenticated user, with modification rights")
2825   @ApiResponses(value = {
2826       @ApiResponse(code = 200, message = "Attachment successfully deleted"),
2827       @ApiResponse(code = 401, message = "User unauthorized to delete this attachment"),
2828       @ApiResponse(code = 404, message = "Attachment with provided ID Not Found"),
2829       @ApiResponse(code = 503, message = "An error occured during the saving process")
2830   })
2831   public Response deleteAttachmentById(
2832       @ApiParam(value = "Identity of the attachment to delete", required = true) @PathParam("id") String id) {
2833     try {
2834       id = AttachmentResource.decode(id);
2835       CalendarEvent ev = this.findEventAttachment(id);
2836       if (ev == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2837       Calendar cal = calendarServiceInstance().getCalendarById(ev.getCalendarId());
2838       if (cal == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2839 
2840       if (Utils.isCalendarEditable(currentUserId(), cal)) {
2841         calendarServiceInstance().removeAttachmentById(id);
2842         return Response.ok().cacheControl(nc).build();
2843       }
2844       return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
2845     } catch (Exception e) {
2846       if(log.isDebugEnabled()) log.debug(e.getMessage());
2847     }
2848     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2849   }
2850 
2851   /**
2852    * Returns the categories (common and personal categories).
2853    *
2854    * @param offset The starting point when paging the result. Default is *0*.
2855    * 
2856    * @param limit Maximum number of categories returned.
2857    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
2858    * 
2859    * @param fields Comma-separated list of selective category attributes to be returned. All returned if not specified.
2860    * 
2861    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
2862    *        If not specified, only JSON object is returned.
2863    * 
2864    * @request  GET: http://localhost:8080/rest/private/v1/calendar/categories?fields=id,name
2865    * 
2866    * @format JSON
2867    * 
2868    * @response 
2869    * {
2870    *   "limit": 10,
2871    *   "data": [
2872    *     {
2873    *       "name": "defaultEventCategoryNameAll",
2874    *       "href": "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll",
2875    *       "id": "defaultEventCategoryIdAll"
2876    *     },
2877    *     {...}
2878    *   ],
2879    *   "size": 6,
2880    *   "offset": 0
2881    * }
2882    * 
2883    * @return  List of categories.
2884    * 
2885    * @authentication
2886    *
2887    * @anchor CalendarRestApi.getEventCategories
2888    */
2889   @SuppressWarnings({ "rawtypes", "unchecked" })
2890   @GET
2891   @RolesAllowed("users")
2892   @Path("/categories")
2893   @Produces(MediaType.APPLICATION_JSON)
2894   @ApiOperation(
2895       value = "Returns the categories accessible to the user",
2896       notes = "Returns the categories if a user is authenticated (the common categories + the personal categories)")
2897   @ApiResponses(value = {
2898       @ApiResponse(code = 200, message = "Successful retrieval of all event categories"),
2899       @ApiResponse(code = 404, message = "No categories Found"),
2900       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2901   })
2902   public Response getEventCategories(
2903         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
2904         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
2905         @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
2906         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
2907         @Context UriInfo uriInfo) {
2908     limit = parseLimit(limit);
2909 
2910     try {
2911       List<EventCategory> ecData = calendarServiceInstance().getEventCategories(currentUserId(), offset, limit);
2912       if(ecData == null || ecData.isEmpty()) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2913       Collection data = new ArrayList();
2914 
2915       String basePath = getBasePath(uriInfo);
2916       for(EventCategory ec:ecData) {
2917         data.add(extractObject(new CategoryResource(ec, basePath), fields));
2918       }
2919 
2920       CollectionResource resource = new CollectionResource(data, ecData.size());
2921       resource.setOffset(offset);
2922       resource.setLimit(limit);
2923 
2924       if (jsonp != null) {
2925         JsonValue json = new JsonGeneratorImpl().createJsonObject(resource);
2926         StringBuilder sb = new StringBuilder(jsonp);
2927         sb.append("(").append(json).append(");");
2928         return Response.ok(sb.toString(), new MediaType("text", "javascript")).header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, resource.getSize())).cacheControl(nc).build();
2929       }
2930 
2931       //
2932       return Response.ok(resource, MediaType.APPLICATION_JSON).header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, resource.getSize())).cacheControl(nc).build();
2933     } catch (Exception e) {
2934       if(log.isDebugEnabled()) log.debug(e.getMessage());
2935     }
2936     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
2937   }
2938 
2939   /**
2940    * Returns a category specified by id if it is a common category or is a personal category of the user.
2941    * 
2942    * @param id Identity of the category.
2943    * 
2944    * @param fields Comma-separated list of selective category attributes to be returned. All returned if not specified.
2945    * 
2946    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
2947    *        If not specified, only JSON object is returned.
2948    * 
2949    * @request  GET: http://localhost:8080/rest/private/v1/calendar/categories/cat123?fields=id,name
2950    * 
2951    * @format JSON
2952    * 
2953    * @response 
2954    * {
2955    *   "name": "defaultEventCategoryNameAll",
2956    *   "href": "http://localhost:8080/rest/private/v1/calendar/categories/defaultEventCategoryIdAll",
2957    *   "id": "defaultEventCategoryIdAll"
2958    * }
2959    *  
2960    * @return  A category in JSON.
2961    * 
2962    * @authentication
2963    *
2964    * @anchor CalendarRestApi.getEventCategoryById
2965    */
2966   @GET
2967   @RolesAllowed("users")
2968   @Path("/categories/{id}")
2969   @Produces(MediaType.APPLICATION_JSON)
2970   @ApiOperation(
2971       value = "Returns an event category identified by its ID",
2972       notes = "Returns the event category by id if it belongs to the user")
2973   @ApiResponses(value = {
2974       @ApiResponse(code = 200, message = "Successful retrieval of the event category"),
2975       @ApiResponse(code = 404, message = "Event category with provided ID Not Found"),
2976       @ApiResponse(code = 503, message = "An error occurred during the saving process")
2977   })
2978   public Response getEventCategoryById(
2979       @ApiParam(value = "Identity of the event category to find", required = true) @PathParam("id") String id,
2980       @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
2981       @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
2982       @Context UriInfo uriInfo,
2983       @Context Request request) {
2984     try {
2985       List<EventCategory> data = calendarServiceInstance().getEventCategories(currentUserId());
2986       if(data == null || data.isEmpty()) {
2987         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2988       }
2989       EventCategory category = null;
2990       for (int i = 0; i < data.size(); i++) {
2991         if(id.equals(data.get(i).getId())) {
2992           category = data.get(i);
2993           break;
2994         }
2995       }
2996 
2997       if(category == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
2998 
2999       Date lastModified = new Date(category.getLastModified());
3000       ResponseBuilder preCondition = request.evaluatePreconditions(lastModified);
3001       if (preCondition != null) {
3002         return preCondition.build();
3003       }
3004 
3005       CategoryResource categoryR = new CategoryResource(category, getBasePath(uriInfo));
3006       Object resource = extractObject(categoryR, fields);
3007       if (jsonp != null) {
3008         String json = null;
3009         if (resource instanceof Map) json = new JSONObject((Map<?, ?>)resource).toString();
3010         else {
3011           JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
3012           json = generatorImpl.createJsonObject(resource).toString();
3013         }
3014         StringBuilder sb = new StringBuilder(jsonp);
3015         sb.append("(").append(json).append(");");
3016         return Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(cc).lastModified(lastModified).build();
3017       }
3018 
3019       //
3020       return Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(cc).lastModified(lastModified).build();
3021     } catch (Exception e) {
3022       if(log.isDebugEnabled()) log.debug(e.getMessage());
3023     }
3024     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3025   }
3026 
3027   /**
3028    *
3029    * Gets a feed with the given id. The user must be the owner of the feed.
3030    *  
3031    * @param id The title of the feed.
3032    * 
3033    * @param fields Comma-separated list of selective feed attributes to be returned. All returned if not specified.
3034    * 
3035    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
3036    *        If not specified, only JSON object is returned.
3037    * 
3038    * @param expand Used to ask for more attributes of a sub-resource, instead of its link only. 
3039    *        This is a comma-separated list of attribute names. For example: expand=calendar,categories. In case of collections, 
3040    *        you can specify offset (default: 0), limit (default: *defaultLimit*). For example, expand=categories(1,5).
3041    *        Instead of: 
3042    *        {
3043    *            "id": "...", 
3044    *            "calendar": "http://localhost:8080/rest/private/v1/calendar/calendars/demo-defaultCalendarId"
3045    *            ...
3046    *        }
3047    *        It returns:
3048    *        {
3049    *            "id": "...", 
3050    *            "calendar": 
3051    *            {
3052    *              "id": "...",
3053    *              "name":"demo-defaultId",
3054    *              ...
3055    *            }
3056    *            ...
3057    *        }
3058    * 
3059    * @request  GET: http://localhost:8080/rest/private/v1/calendar/feeds/feed123?fields=id,name
3060    * 
3061    * @format JSON
3062    * 
3063    * @response 
3064    * {
3065    *   "calendars": [
3066    *     "http://localhost:8080/rest/private/v1/calendar/calendars/john-defaultCalendarId"
3067    *   ],
3068    *   "calendarIds": [
3069    *     "john-defaultCalendarId"
3070    *   ],
3071    *   "rss": "/v1/calendar/feeds/Calendar_Feed/rss",
3072    *   "name": "Calendar_Feed",
3073    *   "href": "http://localhost:8080/rest/private/v1/calendar/feeds/Calendar_Feed",
3074    *   "id": "Calendar_Feed"
3075    * }
3076    *  
3077    * @return  Feed in JSON.
3078    * 
3079    * @authentication
3080    *
3081    * @anchor CalendarRestApi.getFeedById
3082    */
3083   @GET
3084   @RolesAllowed("users")
3085   @Path("/feeds/{id}")
3086   @Produces(MediaType.APPLICATION_JSON)
3087   @ApiOperation(
3088       value = "Returns a feed identified by its ID",
3089       notes = "Returns the feed with the given ID if the authenticated user is the owner of the feed")
3090   @ApiResponses(value = {
3091       @ApiResponse(code = 200, message = "Successful retrieval of the feed"),
3092       @ApiResponse(code = 404, message = "Feed with provided ID Not Found"),
3093       @ApiResponse(code = 503, message = "An error occurred during the saving process")
3094   })
3095   public Response getFeedById(
3096       @ApiParam(value = "Title of the feed to find", required = true) @PathParam("id") String id,
3097       @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
3098       @ApiParam(value = "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", required = false) @QueryParam("expand") String expand,
3099       @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
3100       @Context UriInfo uriInfo,
3101       @Context Request request) {
3102     try {
3103       FeedData feed = null;
3104       for (FeedData feedData : calendarServiceInstance().getFeeds(currentUserId())) {
3105         if (feedData.getTitle().equals(id)) {
3106           feed = feedData;
3107           break;
3108         }
3109       }
3110       if(feed == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3111       byte[] data = feed.getContent();
3112 
3113       byte[] hashCode = digest(data).getBytes();
3114       EntityTag tag = new EntityTag(new String(hashCode));
3115       ResponseBuilder preCondition = request.evaluatePreconditions(tag);
3116       if (preCondition != null) {
3117         return preCondition.build();
3118       }
3119 
3120       SyndFeedInput input = new SyndFeedInput();
3121       SyndFeed syndFeed = input.build(new XmlReader(new ByteArrayInputStream(data)));
3122       List<SyndEntry> entries = new ArrayList<SyndEntry>(syndFeed.getEntries());
3123       List<String> calIds = new ArrayList<String>();
3124       for (SyndEntry entry : entries) {
3125         String calendarId = entry.getLink().substring(entry.getLink().lastIndexOf("/")+1) ;
3126         calIds.add(calendarId);
3127       }
3128 
3129       Object resource = buildFeedResource(feed, calIds, uriInfo, expand, fields);
3130       return buildJsonP(resource, jsonp).cacheControl(cc).tag(tag).build();
3131     } catch (Exception e) {
3132       if(log.isDebugEnabled()) log.debug(e.getMessage());
3133     }
3134     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3135   }
3136 
3137   /**
3138    * Updates a feed with the given id. The user must be the owner of the feed. 
3139    * 
3140    * This accepts HTTP PUT request, with JSON object (feedResource) in the request body, and feed id in the path.
3141    * All the feed attributes are optional, any absent/invalid attributes will be ignored.
3142    * *id* and *href* are auto-generated and cannot be edited by the users.
3143    * For example:
3144    *   {
3145    *      "name": "..",
3146    *      "calendarIds": ["...", "..."]
3147    *   }
3148    *  
3149    * @param id The title of the feed.
3150    * 
3151    * @param feedResource JSON object contains attributes of the feed to be updated, all the attributes are optional.
3152    * 
3153    * @request  PUT: http://localhost:8080/rest/private/v1/calendar/feeds/feed123
3154    * 
3155    * @response  HTTP status code:
3156    *            200 if updated successfully, 404 if feed not found, 503 if any error during save process.
3157    * 
3158    * @return HTTP status code.
3159    * 
3160    * @authentication
3161    *
3162    * @anchor CalendarRestApi.updateFeedById
3163    */
3164   @PUT
3165   @RolesAllowed("users")
3166   @Path("/feeds/{id}")
3167   @ApiOperation(
3168       value = "Updates a feed identified by its ID",
3169       notes = "Updates the feed with the given ID if the authenticated user is the owner of the feed")
3170   @ApiResponses(value = {
3171       @ApiResponse(code = 200, message = "Feed successfully updated"),
3172       @ApiResponse(code = 404, message = "Feed with provided ID Not Found"),
3173       @ApiResponse(code = 503, message = "An error occurred during the saving process")
3174   })
3175   public Response updateFeedById(
3176       @ApiParam(value = "Title of the feed to update", required = true) @PathParam("id") String id,
3177       FeedResource feedResource) {
3178     try {
3179       FeedData feed = null;
3180       for (FeedData feedData : calendarServiceInstance().getFeeds(currentUserId())) {
3181         if (feedData.getTitle().equals(id)) {
3182           feed = feedData;
3183           break;
3184         }
3185       }
3186 
3187       if (feed == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3188 
3189       LinkedHashMap<String, Calendar> calendars = new LinkedHashMap<String, Calendar>();
3190       if (feedResource.getCalendarIds() != null) {
3191         for (String calendarId : feedResource.getCalendarIds()) {
3192           Calendar calendar = calendarServiceInstance().getCalendarById(calendarId);
3193           int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), calendarId);
3194           switch (calType) {
3195           case Calendar.TYPE_PRIVATE:
3196             calendars.put(Calendar.TYPE_PRIVATE + Utils.COLON + calendarId, calendar);
3197             break;
3198           case Calendar.TYPE_PUBLIC:
3199             calendars.put(Calendar.TYPE_PUBLIC + Utils.COLON + calendarId, calendar);
3200             break;
3201           case Calendar.TYPE_SHARED:
3202             calendars.put(Calendar.TYPE_SHARED + Utils.COLON + calendarId, calendar);
3203             break;
3204           default:
3205             break;
3206           }
3207         }
3208       }
3209 
3210       //
3211       calendarServiceInstance().removeFeedData(currentUserId(),id);
3212 
3213       RssData rssData = new RssData();
3214       if (feedResource.getName() != null) {
3215         rssData.setName(feedResource.getName() + Utils.RSS_EXT) ;
3216         rssData.setTitle(feedResource.getName()) ;
3217         rssData.setDescription(feedResource.getName());
3218       }
3219       rssData.setUrl(feed.getUrl()) ;
3220       rssData.setLink(feed.getUrl());
3221       rssData.setVersion("rss_2.0") ;
3222       //
3223       calendarServiceInstance().generateRss(currentUserId(), calendars, rssData);
3224 
3225       return Response.ok().cacheControl(nc).build();
3226     } catch (Exception e) {
3227       if(log.isDebugEnabled()) log.debug(e.getMessage());
3228     }
3229     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3230   }
3231 
3232   /**
3233    * Deletes a feed with the given id. The user must be the owner of the feed.
3234    * 
3235    * @param id The title of the feed.
3236    * 
3237    * @request  DELETE: http://localhost:8080/rest/private/v1/calendar/feeds/feed123
3238    * 
3239    * @response  HTTP status code:
3240    *            200 if delete successfully, 503 if any error during save process.
3241    * 
3242    * @return HTTP status code.
3243    * 
3244    * @authentication
3245    *
3246    * @anchor CalendarRestApi.deleteFeedById
3247    */
3248   @DELETE
3249   @RolesAllowed("users")
3250   @Path("/feeds/{id}")
3251   @ApiOperation(
3252       value = "Deletes a feed identified by its ID",
3253       notes = "Deletes the feed with the given ID if the authenticated user is the owner of the feed")
3254   @ApiResponses(value = {
3255       @ApiResponse(code = 200, message = "Feed successfully deleted"),
3256       @ApiResponse(code = 503, message = "An error occurred during the saving process")
3257   })
3258   public Response deleteFeedById(
3259       @ApiParam(value = "Title of the feed to delete", required = true) @PathParam("id") String id) {
3260     try {
3261       calendarServiceInstance().removeFeedData(currentUserId(),id);
3262       return Response.ok().cacheControl(nc).build();
3263     } catch (Exception e) {
3264       if(log.isDebugEnabled()) log.debug(e.getMessage());
3265     }
3266     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3267   }
3268 
3269   /**
3270    *  Gets the RSS stream of a feed with the given id. The user must be the owner of the feed.
3271    *   
3272    * @param id The title of the feed.
3273    * 
3274    * @request  GET: http://localhost:8080/rest/private/v1/calendar/feeds/feed123/rss
3275    * 
3276    * @format application/xml
3277    * 
3278    * @response RSS
3279    *  
3280    * @return  Calendar RSS.
3281    * 
3282    * @authentication
3283    *
3284    * @anchor CalendarRestApi.getRssFromFeed
3285    */
3286   @GET
3287   @RolesAllowed("users")
3288   @Path("/feeds/{id}/rss")
3289   @Produces(MediaType.APPLICATION_XML)
3290   @ApiOperation(
3291       value = "Gets the RSS stream of the feed with the given ID",
3292       notes = "Returns the RSS stream if:<br/>"
3293           + "- the calendar is public<br/>"
3294           + "- the authenticated user is the owner of the calendar<br/>"
3295           + "- the authenticated user belongs to the group of the calendar<br/>"
3296           + "- the calendar has been shared with the authenticated user or with a group of the authenticated user")
3297   @ApiResponses(value = {
3298       @ApiResponse(code = 200, message = "Successful retrieval of RSS stream from the feed"),
3299       @ApiResponse(code = 404, message = "Feed with provided ID Not Found"),
3300       @ApiResponse(code = 503, message = "An error occurrred during the saving process")
3301   })
3302   public Response getRssFromFeed(
3303       @ApiParam(value = "Title of the feed", required = true) @PathParam("id") String id,
3304       @Context UriInfo uri,
3305       @Context Request request) {
3306     try {
3307       String username = currentUserId();
3308       String feedname = id;
3309       FeedData feed = null;
3310       for (FeedData feedData : calendarServiceInstance().getFeeds(username)) {
3311         if (feedData.getTitle().equals(feedname)) {
3312           feed = feedData;
3313           break;
3314         }
3315       }
3316 
3317       if (feed == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3318 
3319       SyndFeedInput input = new SyndFeedInput();
3320       SyndFeed syndFeed = input.build(new XmlReader(new ByteArrayInputStream(feed.getContent())));
3321       List<SyndEntry> entries = new ArrayList<SyndEntry>(syndFeed.getEntries());
3322       List<CalendarEvent> events = new ArrayList<CalendarEvent>();
3323       List<Calendar> calendars = new ArrayList<Calendar>();
3324       for (SyndEntry entry : entries) {
3325         String calendarId = entry.getLink().substring(entry.getLink().lastIndexOf("/")+1) ;
3326         calendars.add(calendarServiceInstance().getCalendarById(calendarId));
3327       }
3328 
3329       for (Calendar cal : calendars) {
3330         if (cal.getPublicUrl() != null || this.hasViewCalendarPermission(cal, username)) {
3331           int calType = calendarServiceInstance().getTypeOfCalendar(username, cal.getId());
3332           switch (calType) {
3333             case Calendar.TYPE_PRIVATE:
3334               events.addAll(calendarServiceInstance().getUserEventByCalendar(username, Arrays.asList(cal.getId())));
3335               break;
3336             case Calendar.TYPE_SHARED:
3337               events.addAll(calendarServiceInstance().getSharedEventByCalendars(username, Arrays.asList(cal.getId())));
3338               break;
3339             case Calendar.TYPE_PUBLIC:
3340               EventQuery eventQuery = new EventQuery();
3341               eventQuery.setCalendarId(new String[] { cal.getId() });
3342               events.addAll(calendarServiceInstance().getPublicEvents(eventQuery));
3343               break;
3344             default:
3345               break;
3346           }
3347         }
3348       }
3349 
3350       if(events.size() == 0) {
3351         return Response.status(HTTPStatus.NOT_FOUND).entity("Feed " + feedname + "is removed").cacheControl(nc).build();
3352       }
3353       String xml = makeFeed(username, events, feed, uri);
3354 
3355       byte[] hashCode = digest(xml.getBytes()).getBytes();
3356       EntityTag tag = new EntityTag(new String(hashCode));
3357       ResponseBuilder preCondition = request.evaluatePreconditions(tag);
3358       if (preCondition != null) {
3359         return preCondition.build();
3360       }
3361 
3362       return Response.ok(xml, MediaType.APPLICATION_XML).cacheControl(cc).tag(tag).build();
3363     } catch (Exception e) {
3364       if(log.isDebugEnabled()) log.debug(e.getMessage());
3365     }
3366     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3367   }
3368 
3369   /**
3370    * Returns an invitation with specified id if one of conditions:
3371    * The authenticated user is the participant of the invitation,
3372    * OR the user has edit permission on the calendar of the event of the invitation.
3373    * 
3374    * @param id Identity of the invitation.
3375    * 
3376    * @param fields Comma-separated list of selective invitation attributes to be returned. All returned if not specified.
3377    * 
3378    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
3379    *        If not specified, only JSON object is returned.
3380    * 
3381    * @param expand Use expand=event to get more event attributes instead of only link.
3382    * 
3383    * @request  GET: http://localhost:8080/rest/private/v1/calendar/invitations/evt123:root
3384    * 
3385    * @format  JSON
3386    * 
3387    * @response 
3388    * {
3389    *   "participant": "root",
3390    *   "event": "http://localhost:8080/rest/private/v1/calendar/events/Event9b014f9e7f00010166296f35cf2af06b",
3391    *   "status": "",
3392    *   "href": "http://localhost:8080/rest/private/v1/calendar/invitations/Event9b014f9e7f00010166296f35cf2af06b:root",
3393    *   "id": "Event9b014f9e7f00010166296f35cf2af06b:root"
3394    * }
3395    *  
3396    * @return  Invitation as JSON.
3397    * 
3398    * @authentication
3399    *
3400    * @anchor CalendarRestApi.getInvitationById
3401    */
3402   @GET
3403   @RolesAllowed("users")
3404   @Path("/invitations/{id}")
3405   @Produces(MediaType.APPLICATION_JSON)
3406   @ApiOperation(
3407       value = "Returns an invitation identified by its ID",
3408       notes = "Returns an invitation with specified id if:<br/>"
3409           + "- the authenticated user is the participant of the invitation<br/>"
3410           + "- the authenticated user has edit rights on the calendar of the event of the invitation")
3411   @ApiResponses(value = {
3412       @ApiResponse(code = 200, message = "Successful retrieval of the invitation"),
3413       @ApiResponse(code = 404, message = "Invitation with provided ID Not Found")
3414   })
3415   public Response getInvitationById(
3416       @ApiParam(value = "Identity of the invitation to find", required = true) @PathParam("id") String id,
3417       @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
3418       @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
3419       @ApiParam(value = "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", required = false) @QueryParam("expand") String expand,
3420       @Context UriInfo uriInfo,
3421       @Context Request request) throws Exception {
3422     CalendarService service = calendarServiceInstance();
3423     EventDAO evtDAO = service.getEventDAO();
3424     String username = currentUserId();
3425 
3426     Invitation invitation = evtDAO.getInvitationById(id);
3427     if (invitation == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3428 
3429     EntityTag tag = new EntityTag(String.valueOf(invitation.hashCode()));
3430     ResponseBuilder preCondition = request.evaluatePreconditions(tag);
3431     if (preCondition != null) {
3432       return preCondition.build();
3433     }
3434 
3435     //dont return invitation if user is not participant and not have edit permission
3436     if (!username.equals(invitation.getParticipant())) {
3437       CalendarEvent event = service.getEventById(invitation.getEventId());
3438       Calendar calendar = service.getCalendarById(event.getCalendarId());
3439 
3440       if (!Utils.isCalendarEditable(username, calendar)) {
3441         return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3442       }
3443     }
3444 
3445     Object resource = buildInvitationResource(invitation, uriInfo, expand, fields);
3446     return buildJsonP(resource, jsonp).cacheControl(cc).tag(tag).build();
3447   }
3448 
3449   /**
3450    * Replies to an invitation specified by id. The user must be the invitee.
3451    *  
3452    * @param id Identity of the invitation.
3453    * 
3454    * @param status New status to update ("", "maybe", "yes", "no").
3455    * 
3456    * @request  PUT: http://localhost:8080/rest/private/v1/calendar/invitations/evt123:root
3457    * 
3458    * @response  HTTP status code:
3459    *            200 if updated successfully, 404 if invitation not found, 400 if status is invalid,
3460    *            401 if the user does not have permission, 503 if any error during save process.
3461    * 
3462    * @return  HTTP status code.
3463    * 
3464    * @authentication
3465    *
3466    * @anchor CalendarRestApi.updateInvitationById
3467    */
3468   @PUT
3469   @RolesAllowed("users")
3470   @Path("/invitations/{id}")
3471   @ApiOperation(
3472       value = "Updates an invitation identified by its ID",
3473       notes = "Update the invitation if the authenticated user is the participant of the invitation.<br/>"
3474           + "This entry point only allow http PUT request, with id of invitation in the path, and the status")
3475   @ApiResponses(value = {
3476       @ApiResponse(code = 200, message = "Invitation successfully updated"),
3477       @ApiResponse(code = 400, message = "Bad Request: Status invalid"),
3478       @ApiResponse(code = 401, message = "User unauthorized to update the invitation"),
3479       @ApiResponse(code = 404, message = "Invitation with provided ID Not Found"),
3480       @ApiResponse(code = 503, message = "An error occurred during the saving process")
3481   })
3482   public Response updateInvitationById(
3483       @ApiParam(value = "Identity of the invitation to update", required = true) @PathParam("id") String id,
3484       @ApiParam(value = "New status to update", allowableValues = "['', 'maybe', 'yes', 'no']", required = true) @QueryParam("status") String status) {
3485     if (Arrays.binarySearch(INVITATION_STATUS, status) < 0) {
3486       return buildBadResponse(new ErrorResource("status must be one of: " + StringUtils.join(INVITATION_STATUS, ","), "status"));
3487     }
3488     CalendarService service = calendarServiceInstance();
3489     EventDAO evtDAO = service.getEventDAO();
3490     String username = currentUserId();
3491 
3492     Invitation invitation = evtDAO.getInvitationById(id);
3493     if (invitation != null) {
3494       //Update only if user is participant
3495       if (invitation.getParticipant().equals(username)) {
3496         evtDAO.updateInvitation(id, status);
3497         return Response.ok().cacheControl(nc).build();
3498       } else {
3499         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
3500       }
3501     } else {
3502       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3503     }
3504   }
3505 
3506   /**
3507    * Deletes an invitation with specified id. The authenticated user must have edit permission on the calendar.
3508    *  
3509    * @param id Identity of the invitation.
3510    * 
3511    * @request  DELETE: http://localhost:8080/rest/private/v1/calendar/invitations/evt123:root
3512    * 
3513    * @response  HTTP status code:
3514    *            200 if deleted successfully, 404 if invitation not found,
3515    *            401 if the user does not have permission, 503 if any error during save process.
3516    * 
3517    * @return    HTTP status code.
3518    * 
3519    * @authentication
3520    *
3521    * @anchor CalendarRestApi.deleteInvitationById
3522    */
3523   @DELETE
3524   @RolesAllowed("users")
3525   @Path("/invitations/{id}")
3526   @ApiOperation(
3527       value = "Deletes an invitation identified by its ID",
3528       notes = "Deletes an invitation with specified id if the authenticated user has edit rights on the calendar of the event of the invitation")
3529   @ApiResponses(value = {
3530       @ApiResponse(code = 200, message = "Invitation deleted successfully"),
3531       @ApiResponse(code = 401, message = "User unauthorized to delete this invitation"),
3532       @ApiResponse(code = 404, message = "Invitation with provided ID Not Found"),
3533       @ApiResponse(code = 503, message = "An error occurred during the saving process")
3534   })
3535   public Response deleteInvitationById(
3536       @ApiParam(value = "Identity of the invitation to delete", required = true) @PathParam("id") String id) throws Exception {
3537     CalendarService calService = calendarServiceInstance();
3538     EventDAO evtDAO = calService.getEventDAO();
3539     String username = currentUserId();
3540 
3541     Invitation invitation = evtDAO.getInvitationById(id);
3542     if (invitation == null) return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3543 
3544     CalendarEvent event = calService.getEventById(invitation.getEventId());
3545     Calendar calendar = calService.getCalendarById(event.getCalendarId());
3546 
3547     if (Utils.isCalendarEditable(username, calendar)) {
3548       evtDAO.removeInvitation(id);
3549       return Response.ok().cacheControl(nc).build();
3550     } else {
3551       return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
3552     }
3553   }
3554 
3555   /**
3556    * Returns invitations of an event specified by id when:
3557    * the authenticated user is the participant of the invitation,
3558    * OR the authenticated user has edit permission on the calendar of the event.
3559    * 
3560    * @param id Identity of the event.
3561    *
3562    * @param status Filter the invitations by this status if specified.
3563    * 
3564    * @param offset The starting point when paging the result. Default is *0*.
3565    * 
3566    * @param limit Maximum number of invitations returned.
3567    *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
3568    * 
3569    * @param returnSize Default is *false*. If set to *true*, the total number of matched invitations will be returned in JSON,
3570    *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
3571    * 
3572    * @param fields Comma-separated list of selective invitation attributes to be returned. All returned if not specified.
3573    * 
3574    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
3575    *        If not specified, only JSON object is returned.
3576    * 
3577    * @param expand Use expand=event to get more event attributes instead of only link.
3578    * 
3579    * @request  GET: http://localhost:8080/rest/private/v1/calendar/events/evt123/invitations
3580    * 
3581    * @format  JSON
3582    * 
3583    * @response 
3584    * {
3585    *   "limit": 10,
3586    *   "data": [
3587    *     {
3588    *       "participant": "john",
3589    *       "event": "http://localhost:8080/rest/private/v1/calendar/events/Event9b014f9e7f00010166296f35cf2af06b",
3590    *       "status": "",
3591    *       "href": "http://localhost:8080/rest/private/v1/calendar/invitations/Event9b014f9e7f00010166296f35cf2af06b:john",
3592    *       "id": "Event9b014f9e7f00010166296f35cf2af06b:john"
3593    *     },
3594    *     {
3595    *       "participant": "root",
3596    *       "event": "http://localhost:8080/rest/private/v1/calendar/events/Event9b014f9e7f00010166296f35cf2af06b",
3597    *       "status": "",
3598    *       "href": "http://localhost:8080/rest/private/v1/calendar/invitations/Event9b014f9e7f00010166296f35cf2af06b:root",
3599    *       "id": "Event9b014f9e7f00010166296f35cf2af06b:root"
3600    *     }
3601    *   ],
3602    *   "size": -1,
3603    *   "offset": 0
3604    * }
3605    * 
3606    * @return  Invitations as JSON.
3607    * 
3608    * @authentication
3609    *
3610    * @anchor  CalendarRestApi.getInvitationsFromEvent
3611    */
3612   @SuppressWarnings({ "rawtypes", "unchecked" })
3613   @GET
3614   @RolesAllowed("users")
3615   @Path("/events/{id}/invitations/")
3616   @Produces(MediaType.APPLICATION_JSON)
3617   @ApiOperation(
3618       value = "Returns the invitations of an event identified by its ID",
3619       notes = "Returns invitations of an event with specified id when:<br/>"
3620           + "- the authenticated user is the participant of the invitation<br/>"
3621           + "- the authenticated user has edit rights on the calendar of the event of the invitation")
3622   @ApiResponses(value = {
3623       @ApiResponse(code = 200, message = "Successful retrieval of all invitations from the event"),
3624           @ApiResponse(code = 404, message = "Event with provided ID Not Found")
3625   })
3626   public Response getInvitationsFromEvent(
3627         @ApiParam(value = "Identity of the event to search for invitations", required = true) @PathParam("id") String id,
3628         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
3629         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
3630         @ApiParam(value = "Tells the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
3631         @ApiParam(value = "search for this status only", required = false, defaultValue = "If not specified, search invitation of any status ('', 'maybe', 'yes', 'no')") @QueryParam("status") String status,
3632         @ApiParam(value = "This is a list of comma separated property's names of response json object", required = false) @QueryParam("fields") String fields,
3633         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
3634         @ApiParam(value = "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", required = false) @QueryParam("expand") String expand,
3635         @Context UriInfo uriInfo) throws Exception {
3636     limit = parseLimit(limit);
3637     CalendarService calService = calendarServiceInstance();
3638 
3639     CalendarEvent event = calService.getEventById(id);
3640     String username = currentUserId();
3641 
3642     List<Invitation> invitations = Collections.<Invitation>emptyList();
3643     if (event != null) {
3644       //All invitations in event
3645       invitations = new LinkedList<Invitation>(Arrays.asList(event.getInvitations()));
3646 
3647       //Only return user's invitation if calendar is not editable
3648       Calendar calendar = calService.getCalendarById(event.getCalendarId());
3649       if (!Utils.isCalendarEditable(username, calendar)) {
3650         Iterator<Invitation> iter = invitations.iterator();
3651 
3652         while(iter.hasNext()) {
3653           if (!iter.next().getParticipant().equals(username)) {
3654             iter.remove();
3655           }
3656         }
3657       }
3658 
3659       //Return only invitation with specific status
3660       if (status != null) {
3661         Iterator<Invitation> iter = invitations.iterator();
3662         while(iter.hasNext()) {
3663           if (!iter.next().getStatus().equals(status)) {
3664             iter.remove();
3665           }
3666         }
3667       }
3668     }
3669 
3670     List data = new LinkedList();
3671     for (Invitation invitation : Utils.subList(invitations, offset, limit)) {
3672       data.add(buildInvitationResource(invitation, uriInfo, expand, fields));
3673     }
3674     int fullSize = invitations.size();
3675 
3676     CollectionResource ivData = new CollectionResource(data, returnSize ? fullSize : -1);
3677     ivData.setOffset(offset);
3678     ivData.setLimit(limit);
3679 
3680     ResponseBuilder response = buildJsonP(ivData, jsonp);
3681     if (returnSize) {
3682       response.header(HEADER_LINK, buildFullUrl(uriInfo, offset, limit, fullSize));
3683     }
3684     return response.build();
3685   }
3686 
3687   /**
3688    *  Creates an invitation in an event specified by event id, in one of conditions:
3689    *  the authenticated user is the participant of the event,
3690    *  OR the authenticated user has edit permission on the calendar of the event.
3691    *  This accepts invitation resource in request body, for example : {"participant":"root","status":""}.
3692    *  
3693    * 
3694    * @param id Identity of the event.
3695    * 
3696    * @request  POST: http://localhost:8080/rest/private/v1/calendar/events/evt123/invitations
3697    * 
3698    * @response  HTTP status code:
3699    *            201 if created successfully, and HTTP header *location* href that points to the newly created invitation.
3700    *            400 if participant is invalid.
3701    *            404 if event not found.
3702    *            401 if the authenticated user does not have permission.
3703    *            503 if any error during save process.
3704    * 
3705    * @return  HTTP status code.
3706    * 
3707    * @authentication
3708    *
3709    * @anchor CalendarRestApi.createInvitationForEvent
3710    */
3711   @POST
3712   @RolesAllowed("users")
3713   @Path("/events/{id}/invitations/")
3714   @ApiOperation(
3715       value = "Creates an invitation in the event with the given id",
3716       notes = "Creates the invitation only if:<br/>"
3717           + "- the authenticated user is the participant of the invitation<br/>"
3718           + "- the authenticated user has edit rights on the calendar of the event of the invitation")
3719   @ApiResponses(value = {
3720       @ApiResponse(code = 201, message = "Invitation created successfully"),
3721       @ApiResponse(code = 400, message = "Bad Request: invalid participant or status"),
3722       @ApiResponse(code = 401, message = "User unauthorized to create an invitation for this event"),
3723       @ApiResponse(code = 404, message = "Event with provided ID Not Found"),
3724       @ApiResponse(code = 503, message = "An error occurred during the saving process")
3725   })
3726   public Response createInvitationForEvent(
3727       @ApiParam(value = "Identity of the event where the invitation is created", required = true) @PathParam("id") String id,
3728       InvitationResource invitation,
3729       @Context UriInfo uriInfo) throws Exception {
3730     if(invitation == null) {
3731       return buildBadResponse(new ErrorResource("Invitation information must not null", "invitation"));
3732     }
3733     String participant = invitation.getParticipant();
3734     String status = invitation.getStatus();
3735     if (participant == null || participant.trim().isEmpty()) {
3736       return buildBadResponse(new ErrorResource("participant must not null or empty", "participant"));
3737     }
3738     if (Arrays.binarySearch(INVITATION_STATUS, status) < 0) {
3739       return buildBadResponse(new ErrorResource("status must be one of: " + StringUtils.join(INVITATION_STATUS, ","), "status"));
3740     }
3741 
3742     CalendarService service = calendarServiceInstance();
3743     EventDAO evtDAO = service.getEventDAO();
3744     String username = currentUserId();
3745 
3746     CalendarEvent event = service.getEventById(id);
3747     if (event != null) {
3748       Calendar calendar = service.getCalendarById(event.getCalendarId());
3749       if (!Utils.isCalendarEditable(username, calendar)) {
3750         return Response.status(HTTPStatus.UNAUTHORIZED).cacheControl(nc).build();
3751       }
3752 
3753       Invitation invite = evtDAO.createInvitation(id, participant, status);
3754       if (invite != null) {
3755         String location = new StringBuilder(getBasePath(uriInfo)).append(INVITATION_URI).append(invite.getId()).toString();
3756         return Response.status(HTTPStatus.CREATED).header(HEADER_LOCATION, location).cacheControl(nc).build();
3757       } else {
3758         return buildBadResponse(new ErrorResource(participant + " has already been participant, can't create more", "participant"));
3759       }
3760     } else {
3761       return Response.status(HTTPStatus.NOT_FOUND).cacheControl(nc).build();
3762     }
3763   }
3764 
3765     /**
3766      * Suggest participant for specific user.
3767      *
3768      * @param name of participant
3769      *
3770      * @param offset The starting point when paging the result. Default is *0*.
3771      *
3772      * @param limit Maximum number of participants returned.
3773      *        If omitted or exceeds the *query limit* parameter configured for the class, *query limit* is used instead.
3774      *
3775      * @param returnSize Default is *false*. If set to *true*, the total number of matched participants will be returned in JSON,
3776      *        and a "Link" header is added. This header contains "first", "last", "next" and "previous" links.
3777      *
3778      * @param fields Comma-separated list of selective participant attributes to be returned. All returned if not specified.
3779      *
3780      * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
3781      *        If not specified, only JSON object is returned.
3782      *
3783      * @request  {@code GET: http://localhost:8080/rest/private/v1/calendar/participants?name=mary&fields=id,name}
3784      *
3785      * @format  JSON
3786      *
3787      * @response
3788      *
3789      * @return  List of participants in JSON.
3790      *
3791      * @authentication
3792      *
3793      * @anchor  CalendarRestApi.suggestParticipants
3794      */
3795     @SuppressWarnings({ "unchecked", "rawtypes" })
3796     @GET
3797     @RolesAllowed("users")
3798     @Path("/participants/")
3799     @Produces({MediaType.APPLICATION_JSON})
3800     @ApiOperation(
3801             value = "Returns all suggested participants",
3802             notes = "This method lists all the participants a specific user can invite in a calendar.")
3803     @ApiResponses(value = {
3804             @ApiResponse(code = 200, message = "Successful retrieval of all participants"),
3805             @ApiResponse(code = 503, message = "Can't generate JSON file") })
3806     public Response suggestParticipants(
3807         @ApiParam(value = "The participant name to search for", required = false) @QueryParam("name") String name,
3808         @ApiParam(value = "The starting point when paging through a list of entities", required = false, defaultValue = "0") @QueryParam("offset") int offset,
3809         @ApiParam(value = "The maximum number of results when paging through a list of entities, and do not exceed *hardLimit*. If not specified, *defaultLimit* will be used", required = false) @QueryParam("limit") int limit,
3810         @ApiParam(value = "Tell the service if it must return the total size of the returned collection result, and the *link* http headers", required = false, defaultValue = "false") @QueryParam("returnSize") boolean returnSize,
3811         @ApiParam(value = "This is a list of comma-separated property's names of response json object", required = false) @QueryParam("fields") String fields,
3812         @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
3813         @Context UriInfo uri) {
3814     try {
3815         limit = parseLimit(limit);
3816         name = name == null ? "" : name;
3817 
3818         ProfileFilter filter = new ProfileFilter();
3819         filter.setName(name);
3820         filter.setCompany("");
3821         filter.setPosition("");
3822         filter.setSkills("");
3823         filter.setExcludedIdentityList(Collections.emptyList());
3824 
3825         ListAccess<org.exoplatform.social.core.identity.model.Identity> list = identityManager.getIdentitiesByProfileFilter(OrganizationIdentityProvider.NAME, filter, true);
3826 
3827         Collection data = new LinkedList();
3828         if (list != null) {
3829           for (org.exoplatform.social.core.identity.model.Identity identity : list.load(offset, limit)) {
3830             data.add(extractObject(new ParticipantResource(identity), fields));
3831           }
3832         }
3833 
3834         CollectionResource parData = new CollectionResource(data, returnSize ? list.getSize() : -1);
3835         parData.setOffset(offset);
3836         parData.setLimit(limit);
3837 
3838         ResponseBuilder okResult;
3839         if (jsonp != null) {
3840             JsonValue value = new JsonGeneratorImpl().createJsonObject(parData);
3841             StringBuilder sb = new StringBuilder(jsonp);
3842             sb.append("(").append(value).append(");");
3843             okResult = Response.ok(sb.toString(), new MediaType("text", "javascript"));
3844         } else {
3845             okResult = Response.ok(parData, MediaType.APPLICATION_JSON);
3846         }
3847 
3848         if (returnSize) {
3849             okResult.header(HEADER_LINK, buildFullUrl(uri, offset, limit, parData.getSize()));
3850         }
3851 
3852         //
3853         return okResult.cacheControl(nc).build();
3854     } catch (Exception e) {
3855         log.error("Can not suggest participant", e);
3856     }
3857     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3858   }
3859 
3860   /**
3861    * Return availability (free/busy time) of users in period of time.
3862    *
3863    * @param usernames list of user name separated by comma
3864    *
3865    * @param fromDate count from this date
3866    *
3867    * @param toDate count to this date
3868    *
3869    * @param jsonp The name of a JavaScript function to be used as the JSONP callback.
3870    *        If not specified, only JSON object is returned.
3871    *
3872    * @request  {@code GET: http://localhost:8080/rest/private/v1/events/?users=root,mary}
3873    *
3874    * @format  JSON
3875    *
3876    * @response
3877    *
3878    * @return  Map of availability time in JSON.
3879    *
3880    * @authentication
3881    *
3882    * @anchor  CalendarRestApi.getAvailability
3883    */
3884   @SuppressWarnings({ "unchecked", "rawtypes" })
3885   @GET
3886   @RolesAllowed("users")
3887   @Path("/availabilities")
3888   @Produces({MediaType.APPLICATION_JSON})
3889   @ApiOperation(
3890           value = "Return availability of users.",
3891           notes = "(free/busy time)")
3892   @ApiResponses(value = {
3893           @ApiResponse(code = 200, message = "Successful retrieval"),
3894           @ApiResponse(code = 503, message = "Can't generate JSON file") })
3895   public Response getAvailabilities(
3896           @ApiParam(value = "usernames to check availability", required = true) @QueryParam("usernames") String usernames,
3897           @ApiParam(value = "usernames to check availability", required = true) @QueryParam("fromDate") Long fromDate,
3898           @ApiParam(value = "usernames to check availability", required = true) @QueryParam("toDate") Long toDate,
3899           @ApiParam(value = "The name of a JavaScript function to be used as the JSONP callback", required = false) @QueryParam("jsonp") String jsonp,
3900           @Context UriInfo uri) {
3901     try {
3902       Map<String, String> parMap_ = new HashMap<String, String>() ;
3903       parMap_.clear() ;
3904       Map<String, String> tmpMap = new HashMap<String, String>() ;
3905       List<String> newPars = new ArrayList<String>() ;
3906       if(StringUtils.isNotBlank(usernames)) {
3907         for(String par : usernames.split(",")) {
3908           if(orgService.getUserHandler().findUserByName(par) != null)  {
3909             String vl = tmpMap.get(par) ;
3910             parMap_.put(par.trim(), vl) ;
3911             if(vl == null) newPars.add(par.trim()) ;
3912           }
3913         }
3914       }
3915       if(newPars.size() > 0) {
3916         EventQuery eventQuery = new EventQuery() ;
3917 
3918         CalendarSetting setting = calendarServiceInstance().getCalendarSetting(currentUserId());
3919         TimeZone timeZone = DateUtils.getTimeZone(setting.getTimeZone());
3920 
3921         java.util.Calendar from = buildTimeFrom(fromDate, timeZone);
3922         eventQuery.setFromDate(from);
3923 
3924         java.util.Calendar to = buildTimeFrom(toDate, timeZone);
3925         eventQuery.setToDate(to);
3926         //
3927         eventQuery.setParticipants(newPars.toArray(new String[]{})) ;
3928         eventQuery.setNodeType("exo:calendarPublicEvent") ;
3929 
3930         Map<String, String> parsMap = calendarServiceInstance().checkFreeBusy(eventQuery);
3931         parMap_.putAll(parsMap) ;
3932       }
3933 
3934       ResponseBuilder okResult;
3935       if (jsonp != null) {
3936         JsonValue value = new JsonGeneratorImpl().createJsonObject(parMap_);
3937         StringBuilder sb = new StringBuilder(jsonp);
3938         sb.append("(").append(value).append(");");
3939         okResult = Response.ok(sb.toString(), new MediaType("text", "javascript"));
3940       } else {
3941         okResult = Response.ok(parMap_, MediaType.APPLICATION_JSON);
3942       }
3943 
3944       //
3945       return okResult.cacheControl(nc).build();
3946     } catch (Exception e) {
3947       log.error("Can not check availability", e);
3948     }
3949     return Response.status(HTTPStatus.UNAVAILABLE).cacheControl(nc).build();
3950   }
3951 
3952   private java.util.Calendar buildTimeFrom(Long miliseconds, TimeZone timeZone) {
3953     java.util.Calendar cal = java.util.Calendar.getInstance();
3954     cal.setTimeZone(timeZone);
3955     cal.setTimeInMillis(miliseconds);
3956     return cal;
3957   }
3958 
3959   private Response buildBadResponse(ErrorResource error) {
3960     return Response.status(HTTPStatus.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON)
3961         .cacheControl(nc).build();
3962   }
3963 
3964   /**
3965    * Parse date by ISO8601 standard
3966    * if start is null, start is current time
3967    * if end is null, end is current time plus 1 week
3968    * @param start
3969    * @param end
3970    * @return array of start, end date
3971    */
3972   private java.util.Calendar[] parseDate(String start, String end) {
3973     java.util.Calendar from = GregorianCalendar.getInstance();
3974     java.util.Calendar to = GregorianCalendar.getInstance();
3975     if(Utils.isEmpty(start)) {
3976       from = java.util.Calendar.getInstance();
3977       from.set(java.util.Calendar.HOUR, 0);
3978       from.set(java.util.Calendar.MINUTE, 0);
3979       from.set(java.util.Calendar.SECOND, 0);
3980       from.set(java.util.Calendar.MILLISECOND, 0);
3981     } else {
3982       from = ISO8601.parse(start);
3983     }
3984     if(Utils.isEmpty(end)) {
3985       to.add(java.util.Calendar.WEEK_OF_MONTH, 1);
3986       to.set(java.util.Calendar.HOUR, 0);
3987       to.set(java.util.Calendar.MINUTE, 0);
3988       to.set(java.util.Calendar.SECOND, 0);
3989       to.set(java.util.Calendar.MILLISECOND, 0);
3990     } else {
3991       to = ISO8601.parse(end);
3992     }
3993     return new java.util.Calendar[] {from, to};
3994   }
3995 
3996   /**
3997    * Use default value if limit is not set. And do not allow to exceed the hard limit 100.
3998    */
3999   private int parseLimit(int limit) {
4000     if (limit <= 0) {
4001       return defaultLimit;
4002     }
4003 
4004     if (limit > hardLimit) {
4005       return hardLimit;
4006     }
4007 
4008     return limit;
4009   }
4010 
4011   private String getBasePath(UriInfo uriInfo) {
4012     StringBuilder path = new StringBuilder(uriInfo.getBaseUri().toString());
4013     path.append(CAL_BASE_URI);
4014     return path.toString();
4015   }
4016 
4017   private String buildFullUrl(UriInfo uriInfo, int offset, int limit, long fullSize) {
4018       if (fullSize <= 0) {
4019         return "";
4020       }
4021       offset = offset < 0 ? 0 : offset;
4022 
4023       long prev = offset - limit;
4024       prev = offset > 0 && prev < 0 ? 0 : prev;
4025       long prevLimit = offset - prev;
4026       //
4027       StringBuilder sb = new StringBuilder();
4028       if (prev >= 0) {
4029         sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=");
4030         sb.append(prev).append("&limit=").append(prevLimit).append(">").append(Utils.SEMICOLON).append("rel=\"previous\",");
4031       }
4032 
4033       long next = offset + limit;
4034       //
4035       if (next < fullSize) {
4036         sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=");
4037         sb.append(next).append("&limit=").append(limit).append(">").append(Utils.SEMICOLON).append("rel=\"next\",");
4038       }
4039 
4040       //first page
4041       long firstLimit = limit > fullSize ? fullSize : limit;
4042       sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=0&limit=").append(firstLimit).append(">");
4043       sb.append(Utils.SEMICOLON).append("rel=\"first\",");
4044       //last page
4045       long lastIndex = fullSize - (fullSize % firstLimit);
4046       if (lastIndex == fullSize) {
4047         lastIndex = fullSize - firstLimit;
4048       }
4049       if (lastIndex > 0) {
4050         sb.append("<").append(uriInfo.getAbsolutePath()).append("?offset=").append(lastIndex);
4051         sb.append("&limit=").append(fullSize - lastIndex).append(">");
4052         sb.append(Utils.SEMICOLON).append("rel=\"last\"");
4053       }
4054       if (sb.charAt(sb.length() - 1) == ',') {
4055         sb.deleteCharAt(sb.length() - 1);
4056       }
4057 
4058       return sb.toString();
4059   }
4060 
4061   private static CalendarService calendarServiceInstance() {
4062     return (CalendarService)ExoContainerContext.getCurrentContainer()
4063               .getComponentInstanceOfType(CalendarService.class);
4064   }
4065 
4066   /**
4067    *
4068    * @param author : the feed create
4069    * @param events : list of event from data
4070    * @return
4071    * @throws Exception
4072    */
4073   private String makeFeed(String author, List<CalendarEvent> events, FeedData feedData, UriInfo uri) throws Exception {
4074     URI baseUri = uri.getBaseUri();
4075     String baseURL = baseUri.getScheme() + "://" + baseUri.getHost() + ":" + Integer.toString(baseUri.getPort());
4076     String baseRestURL = baseUri.toString();
4077 
4078     SyndFeed feed = new SyndFeedImpl();
4079     feed.setFeedType("rss_2.0");
4080     feed.setTitle(feedData.getTitle());
4081     feed.setLink(baseURL + feedData.getUrl());
4082     feed.setDescription(feedData.getTitle());
4083     List<SyndEntry> entries = new ArrayList<SyndEntry>();
4084     SyndEntry entry;
4085     SyndContent description;
4086     for(CalendarEvent event : events) {
4087       if (Utils.EVENT_NUMBER > 0 && Utils.EVENT_NUMBER <= entries.size()) break;
4088       entry = new SyndEntryImpl();
4089       entry.setTitle(event.getSummary());
4090       entry.setLink(baseRestURL + BASE_EVENT_URL + Utils.SLASH + author + Utils.SLASH + event.getId()
4091                     + Utils.SPLITTER + event.getCalType() + Utils.ICS_EXT);
4092       entry.setAuthor(author) ;
4093       description = new SyndContentImpl();
4094       description.setType(Utils.MIMETYPE_TEXTPLAIN);
4095       description.setValue(event.getDescription());
4096       entry.setDescription(description);
4097       entries.add(entry);
4098       entry.getEnclosures() ;
4099     }
4100     feed.setEntries(entries);
4101     feed.setEncoding("UTF-8") ;
4102     SyndFeedOutput output = new SyndFeedOutput();
4103     String feedXML = output.outputString(feed);
4104     feedXML = StringUtils.replace(feedXML,"&amp;","&");
4105     return feedXML;
4106   }
4107 
4108   private boolean isInGroups(String[] groups) {
4109     Identity identity = ConversationState.getCurrent().getIdentity();
4110     for (String group : groups) {
4111       if (identity.isMemberOf(group)) {
4112         return true;
4113       }
4114     }
4115 
4116     return false;
4117   }
4118 
4119   private boolean hasViewCalendarPermission(Calendar cal, String username) throws Exception {
4120     if (cal.getCalendarOwner() != null && cal.getCalendarOwner().equals(username)) return true;
4121     else if (cal.getGroups() != null) {
4122       return isInGroups(cal.getGroups());
4123     } else if (cal.getViewPermission() != null) {
4124       return Utils.hasPermission(orgService, cal.getViewPermission(), username);
4125     }
4126     return false;
4127   }
4128 
4129   private List<Calendar> findViewableCalendars(String username) throws Exception {
4130     CalendarService service = calendarServiceInstance();
4131     //private calendar
4132     List<Calendar> uCals = service.getUserCalendars(username, true);
4133     //group calendar
4134     Set<String> groupIds = ConversationState.getCurrent().getIdentity().getGroups();
4135     List<GroupCalendarData> gCals = service.getGroupCalendars(groupIds.toArray(new String[groupIds.size()]), true, username);
4136     //shared calendar
4137     GroupCalendarData sCals = service.getSharedCalendars(username, true);
4138     if (sCals != null) {
4139         gCals.add(sCals);
4140     }
4141     //public calendar
4142     Calendar[] publicCals = service.getPublicCalendars().load(0, -1);
4143 
4144     List<Calendar> results = new LinkedList<Calendar>();
4145     results.addAll(Arrays.asList(publicCals));
4146     for (GroupCalendarData data : gCals) {
4147         if (data.getCalendars() != null) {
4148             for (Calendar cal : data.getCalendars()) {
4149                 results.add(cal);
4150             }
4151         }
4152     }
4153     results.addAll(uCals);
4154 
4155     return results;
4156   }
4157 
4158   private List<Calendar> findEditableCalendars(String username) throws Exception {
4159     List<Calendar> calendars = findViewableCalendars(username);
4160     Iterator<Calendar> iter = calendars.iterator();
4161     while (iter.hasNext()) {
4162       if (!Utils.isCalendarEditable(username, iter.next())) {
4163         iter.remove();
4164       }
4165     }
4166     return calendars;
4167   }
4168 
4169   private EventQuery buildEventQuery(String start, String end, String category, List<Calendar> calendars, String calendarPath,
4170                                                String participant, String eventType) {
4171     java.util.Calendar[] dates = parseDate(start, end);
4172 
4173     //Find all invitations that user is participant
4174     EventQuery uQuery = new RestEventQuery();
4175     uQuery.setQueryType(Query.SQL);
4176     if (calendarPath != null) {
4177       uQuery.setCalendarPath(calendarPath);
4178     }
4179     List<String> calIds = new LinkedList<String>();
4180     if (calendars != null) {
4181       for (Calendar cal : calendars) {
4182         calIds.add(cal.getId());
4183       }
4184       uQuery.setCalendarId(calIds.toArray(new String[calIds.size()]));
4185     }
4186     if (category != null) {
4187       uQuery.setCategoryId(new String[] {category});
4188     }
4189     if (participant != null) {
4190       uQuery.setParticipants(new String[] {participant});
4191     }
4192     uQuery.setEventType(eventType);
4193     uQuery.setFromDate(dates[0]);
4194     uQuery.setToDate(dates[1]);
4195     uQuery.setOrderType(Utils.ORDER_TYPE_ASCENDING);
4196     uQuery.setOrderBy(new String[]{Utils.ORDERBY_TITLE});
4197     return uQuery;
4198   }
4199 
4200   private CalendarEvent findEventAttachment(String attachmentID) throws Exception {
4201     int idx = attachmentID.indexOf("/calendars/");
4202     if (idx != -1) {
4203       int calendars =  idx + "/calendars/".length();
4204       int calendar = attachmentID.indexOf('/', calendars) + 1;
4205       int event = attachmentID.indexOf('/', calendar);
4206       if (calendar != -1 && event != -1) {
4207         String eventId = attachmentID.substring(calendar, event);
4208         return calendarServiceInstance().getEventById(eventId);
4209       }
4210     }
4211     return null;
4212   }
4213 
4214   private Object extractObject(Resource iv, String fields) {
4215     if (fields != null && iv != null) {
4216       String[] f = fields.split(",");
4217 
4218       if (f.length > 0) {
4219         JSONObject obj = new JSONObject(iv);
4220         Map<String, Object> map = new HashMap<String, Object>();
4221 
4222         for (String name : f) {
4223           try {
4224             map.put(name, obj.get(name));
4225           } catch (JSONException e) {
4226             log.warn("Can't extract property {} from object {}", name, iv);
4227           }
4228         }
4229         return map;
4230       }
4231     }
4232     return iv;
4233   }
4234 
4235   private String currentUserId() {
4236     return ConversationState.getCurrent().getIdentity().getUserId();
4237   }
4238 
4239   private Response buildEvent(CalendarEvent old, EventResource evObject, Calendar moveToCal) {
4240     if (moveToCal != null) {
4241       old.setCalendarId(moveToCal.getId());
4242       try {
4243         int calType = calendarServiceInstance().getTypeOfCalendar(currentUserId(), moveToCal.getId());
4244         old.setCalType(String.valueOf(calType));
4245       } catch (Exception ex) {
4246         return buildBadResponse(new ErrorResource("Can not get type of calendar " + moveToCal.getId()));
4247       }
4248     }
4249 
4250     try {
4251       if (calendarServiceInstance().isRemoteCalendar(currentUserId(), old.getCalendarId())) {
4252         return buildBadResponse(new ErrorResource("Can not add/update event in remote calendar", "cant-add-event-on-remote-calendar"));
4253       }
4254     } catch (Exception ex) {
4255       return buildBadResponse(new ErrorResource("Can not check remote calendar " + moveToCal.getId(), "cant-check-remote"));
4256     }
4257 
4258     String catId = evObject.getCategoryId();
4259     setEventCategory(old, catId);
4260     if (evObject.getDescription() != null) {
4261       old.setDescription(evObject.getDescription());
4262     }
4263     String eventState = evObject.getAvailability();
4264     if (eventState != null) {
4265       if (Arrays.binarySearch(EVENT_AVAILABILITY, eventState) < 0) {
4266         return buildBadResponse(new ErrorResource("availability must be one of " + StringUtils.join(EVENT_AVAILABILITY, ","), "availability"));
4267       } else {
4268         old.setEventState(eventState);
4269       }
4270     }
4271 
4272     if (evObject.getParticipants() != null) {
4273       old.setParticipant(evObject.getParticipants());
4274       List<String> parStatus = Stream.of(evObject.getParticipants()).map(par -> {
4275         return par + ":";
4276       }).collect(Collectors.toList());
4277       old.setParticipantStatus(parStatus.toArray(new String[parStatus.size()]));
4278     }
4279 
4280     List<Attachment> attachments = new ArrayList<>();
4281     if (old.getAttachment() != null && evObject.getUploadResources() != null) {
4282       for (Attachment att : old.getAttachment()) {
4283         for (org.exoplatform.calendar.ws.bean.UploadResource resource : evObject.getUploadResources()) {
4284           if (att.getName().equals(resource.getName()) && StringUtils.isBlank(resource.getId())) {
4285             attachments.add(att);
4286           }
4287         }
4288       }
4289     }
4290 
4291     if (evObject.getUploadResources() != null) {
4292       Stream.of(evObject.getUploadResources()).forEach((upload) -> {
4293         String uploadId = upload.getId();
4294         UploadResource uploadResource = uploadService.getUploadResource(uploadId);
4295         try {
4296           Attachment attachment = new Attachment();
4297           attachment.setInputStream(new FileInputStream(uploadResource.getStoreLocation()));
4298           attachment.setMimeType(uploadResource.getMimeType());
4299           attachment.setName(uploadResource.getFileName());
4300           long fileSize = ((long)uploadResource.getUploadedSize()) ;
4301           attachment.setSize(fileSize);
4302           attachment.setResourceId(uploadId);
4303 
4304           attachments.add(attachment);
4305         } catch (Exception ex) {
4306           log.error("Can not save event with upload resource: " + uploadId, ex);
4307         }
4308       });
4309     }
4310     old.setAttachment(attachments);
4311 
4312     if (evObject.getRepeat() != null) {
4313       RepeatResource repeat = evObject.getRepeat();
4314       old.setRepeatType(repeat.getType());
4315 
4316       if (repeat.getExclude() != null) {
4317         old.setExceptionIds(Arrays.asList(repeat.getExclude()));
4318       }
4319 
4320       if (repeat.getRepeatOn() != null) {
4321         String[] reptOns = repeat.getRepeatOn().split(",");
4322         for (String on : reptOns) {
4323           if (Arrays.binarySearch(RP_WEEKLY_BYDAY, on) < 0) {
4324             return buildBadResponse(new ErrorResource("repeatOn can only contains " + StringUtils.join(RP_WEEKLY_BYDAY, ","), "repeatOn"));
4325           }
4326         }
4327         old.setRepeatByDay(reptOns);
4328       }
4329       if (repeat.getRepeateBy() != null) {
4330         String[] repeatBy = repeat.getRepeateBy().split(",");
4331         long[] by = new long[repeatBy.length];
4332         for (int i = 0; i < repeatBy.length; i++) {
4333           try {
4334             by[i] = Integer.parseInt(repeatBy[i]);
4335             if (by[i] < 1 || by[i] > 31) {
4336               return buildBadResponse(new ErrorResource("repeatBy must be >= 1 and <= 31", "repeatBy"));
4337             }
4338           } catch (Exception e) {
4339               log.debug(e);
4340           }
4341         }
4342         old.setRepeatByMonthDay(by);
4343       }
4344 
4345       Date untilDate = null;
4346       long count = 0;
4347       if (repeat.getEnd() != null) {
4348         End end = repeat.getEnd();
4349         String val = end.getValue();
4350 
4351         if (RP_END_AFTER.equals(end.getType())) {
4352           try {
4353             count = Long.parseLong(val);
4354           } catch (Exception ex) {
4355             log.debug("Can not set repeat count, count is invalid: {}", val);
4356           }
4357         } else if (RP_END_BYDATE.equals(end.getType())) {
4358           try {
4359             untilDate = ISO8601.parse(val).getTime();
4360           } catch (Exception e) {
4361             log.debug("Can not set repeat until date, date is invalid: {}", val);
4362           }
4363         }
4364       }
4365       old.setRepeatUntilDate(untilDate);
4366       old.setRepeatCount(count);
4367 
4368       int every = repeat.getEvery();
4369       if (every < 1 || every > 30) {
4370         every = 1;
4371       }
4372       old.setRepeatInterval(repeat.getEvery());
4373     } else {
4374       old.setRepeatType(Event.RP_NOREPEAT);
4375     }
4376     old.setRecurrenceId(evObject.getRecurrenceId());
4377 
4378     java.util.Calendar[] fromTo = parseDate(evObject.getFrom(), evObject.getTo());
4379     if (fromTo[0].after(fromTo[1]) || fromTo[0].equals(fromTo[1])) {
4380       return buildBadResponse(new ErrorResource("\"from\" date must be before \"to\" date", "event-date-time-logic"));
4381     }
4382     old.setFromDateTime(fromTo[0].getTime());
4383     if (evObject.getLocation() != null) {
4384       old.setLocation(evObject.getLocation());
4385     }
4386     String priority = evObject.getPriority();
4387     if (priority != null) {
4388       if (Arrays.binarySearch(PRIORITY, priority) < 0) {
4389         return buildBadResponse(new ErrorResource("priority must be one of " + StringUtils.join(PRIORITY, ","), "priority"));
4390       } else {
4391         old.setPriority(evObject.getPriority());
4392       }
4393     }
4394     if (evObject.getReminder() != null) {
4395       Arrays.stream(evObject.getReminder()).forEach(reminder -> {
4396         if (reminder.getReminderOwner() == null) {
4397           reminder.setReminderOwner(currentUserId());
4398         }
4399         try {
4400           if (reminder.getReminderType().equals(Reminder.TYPE_EMAIL) &&
4401                   reminder.getEmailAddress() == null) {
4402             String email = orgService.getUserHandler().findUserByName(currentUserId()).getEmail();
4403             reminder.setEmailAddress(email);
4404           }
4405         } catch (Exception ex) {
4406           log.error("Can not set default email for reminder of user " + currentUserId(), ex);
4407         }
4408         if (reminder.getFromDateTime() == null) {
4409           reminder.setFromDateTime(old.getFromDateTime());
4410         }
4411       });
4412       old.setReminders(Arrays.asList(evObject.getReminder()));
4413     }
4414     String privacy = evObject.getPrivacy();
4415     if (privacy != null) {
4416       if (!CalendarEvent.IS_PRIVATE.equals(privacy) && !CalendarEvent.IS_PUBLIC.equals(privacy)) {
4417         return buildBadResponse(new ErrorResource("privacy can only be public or private", "privacy"));
4418       } else {
4419         old.setPrivate(CalendarEvent.IS_PRIVATE.equals(privacy));
4420       }
4421     }
4422     String subject = evObject.getSubject();
4423     if (subject != null) {
4424       subject = subject.trim();
4425       if (subject.isEmpty()) {
4426         return buildBadResponse(new ErrorResource("subject must not be empty", "subject"));
4427       } else {
4428         old.setSummary(subject);
4429       }
4430     }
4431     old.setToDateTime(fromTo[1].getTime());
4432     return null;
4433   }
4434 
4435   private Response buildEventFromTask(CalendarEvent old, TaskResource evObject) {
4436     String catId = evObject.getCategoryId();
4437     setEventCategory(old, catId);
4438     if (evObject.getNote() != null) {
4439       old.setDescription(evObject.getNote());
4440     }
4441     java.util.Calendar[] fromTo = parseDate(evObject.getFrom(), evObject.getTo());
4442     if (fromTo[0].after(fromTo[1]) || fromTo[0].equals(fromTo[1])) {
4443       return buildBadResponse(new ErrorResource("\"from\" date must be before \"to\" date", "from"));
4444     }
4445     old.setFromDateTime(fromTo[0].getTime());
4446     String priority = evObject.getPriority();
4447     if (priority != null) {
4448       if (Arrays.binarySearch(PRIORITY, priority) < 0) {
4449         return buildBadResponse(new ErrorResource("priority must be one of " + StringUtils.join(PRIORITY, ","), "priority"));
4450       } else {
4451         old.setPriority(evObject.getPriority());
4452       }
4453     }
4454     if (evObject.getReminder() != null) {
4455       old.setReminders(Arrays.asList(evObject.getReminder()));
4456     }
4457     String status = evObject.getStatus();
4458     if (status != null && !status.isEmpty()) {
4459       if (Arrays.binarySearch(TASK_STATUS, status) < 0) {
4460         return buildBadResponse(new ErrorResource("status must be one of " + StringUtils.join(TASK_STATUS, ","), "status"));
4461       } else {
4462         old.setStatus(status);
4463         // Actually task status is saved in eventState field
4464         old.setEventState(status);
4465       }
4466     }
4467     String name = evObject.getName();
4468     if (name != null) {
4469       name = name.trim();
4470       if (name.isEmpty()) {
4471         return buildBadResponse(new ErrorResource("name must not be empty", "name"));
4472       } else {
4473         old.setSummary(evObject.getName());
4474       }
4475     }
4476     old.setToDateTime(fromTo[1].getTime());
4477     if (evObject.getDelegation() != null) {
4478       old.setTaskDelegator(StringUtils.join(evObject.getDelegation(), ","));
4479     }
4480     return null;
4481   }
4482 
4483   private void setEventCategory(CalendarEvent old, String catId) {
4484     if (catId != null) {
4485       try {
4486         EventCategory cat = calendarServiceInstance().getEventCategory(currentUserId(), catId);
4487         if (cat != null) {
4488           old.setEventCategoryId(cat.getId());
4489           old.setEventCategoryName(cat.getName());
4490         }
4491       } catch (Exception e) {
4492         log.debug(e.getMessage(), e);
4493       }
4494     }
4495   }
4496 
4497   private Response buildCalendar(Calendar cal, CalendarResource calR) {
4498     if (calR.getColor() != null) {
4499       cal.setCalendarColor(calR.getColor());
4500     }
4501     if (calR.getOwner() != null) {
4502       cal.setCalendarOwner(calR.getOwner());
4503     }
4504     if (calR.getDescription() != null) {
4505       cal.setDescription(calR.getDescription());
4506     }
4507     Set<String> viewPermissions = new HashSet<String>();
4508     if (calR.getEditPermission() != null && !calR.getEditPermission().isEmpty()) {
4509       cal.setEditPermission(calR.getEditPermission().split(Utils.SEMICOLON));
4510       for (String permission : cal.getEditPermission()) {
4511         viewPermissions.add(permission);
4512       }
4513     }
4514 
4515     if (calR.getViewPermission() != null && !calR.getViewPermission().isEmpty()) {
4516       for (String permission : calR.getViewPermission().split(Utils.SEMICOLON)) {
4517         viewPermissions.add(permission);
4518       }
4519     }
4520     if (viewPermissions.size() > 0) {
4521       cal.setViewPermission(viewPermissions.toArray(new String[viewPermissions.size()]));
4522     }
4523 
4524     if (calR.getGroups() != null) {
4525       cal.setGroups(calR.getGroups());
4526     }
4527     String name = calR.getName();
4528     if (name != null) {
4529       name = name.trim();
4530       if (name.isEmpty() || containSpecialChar(name)) {
4531         return buildBadResponse(new ErrorResource("calendar name is empty or contains special characters", "name"));
4532       } else {
4533         cal.setName(calR.getName());
4534       }
4535     }
4536     if (calR.getPrivateURL() != null) {
4537       cal.setPrivateUrl(calR.getPrivateURL());
4538     }
4539     if (calR.getPublicURL() != null) {
4540       cal.setPublicUrl(calR.getPublicURL());
4541     }
4542     if (calR.getTimeZone() != null) {
4543       cal.setTimeZone(calR.getTimeZone());
4544     }
4545     return null;
4546   }
4547 
4548   private boolean containSpecialChar(String value) {
4549     for (int i = 0; i < value.length(); i++) {
4550       char c = value.charAt(i);
4551       if (Character.isLetter(c) || Character.isDigit(c) || c == '_' || c == '-' || Character.isSpaceChar(c)) {
4552         continue;
4553       }
4554       return true;
4555     }
4556     return false;
4557   }
4558 
4559   private ResponseBuilder buildJsonP(Object resource, String jsonp) throws Exception {
4560     ResponseBuilder response = null;
4561     if (jsonp != null) {
4562       String json = null;
4563       if (resource instanceof Map) json = new JSONObject((Map<?, ?>)resource).toString();
4564       else {
4565         JsonGeneratorImpl generatorImpl = new JsonGeneratorImpl();
4566         json = generatorImpl.createJsonObject(resource).toString();
4567       }
4568       StringBuilder sb = new StringBuilder(jsonp);
4569       sb.append("(").append(json).append(");");
4570       response = Response.ok(sb.toString(), new MediaType("text", "javascript")).cacheControl(nc);
4571     } else {
4572       response = Response.ok(resource, MediaType.APPLICATION_JSON).cacheControl(nc);
4573     }
4574 
4575     return response;
4576   }
4577 
4578   private Object buildTaskResource(CalendarEvent event,
4579                                    UriInfo uriInfo,
4580                                    String expand,
4581                                    String fields) throws Exception {
4582     CalendarService service = calendarServiceInstance();
4583     String basePath = getBasePath(uriInfo);
4584     TaskResource evtResource = new TaskResource(event, basePath);
4585 
4586     List<Expand> expands = Expand.parse(expand);
4587     for (Expand exp : expands) {
4588       if ("calendar".equals(exp.getField())) {
4589         Calendar cal = service.getCalendarById(event.getCalendarId());
4590         setCalType(cal);
4591         evtResource.setCal(new CalendarResource(cal, basePath));
4592       } else if ("categories".equals(exp.getField())) {
4593         String categoryId = event.getEventCategoryId();
4594         if (categoryId != null) {
4595           EventCategory evCat = service.getEventCategory(currentUserId(), categoryId);
4596           if (evCat != null) {
4597             CategoryResource[] catRs = new CategoryResource[] {new CategoryResource(evCat, basePath)};
4598             evtResource.setCats(Utils.<CategoryResource>subArray(catRs, exp.getOffset(), exp.getLimit()));
4599           }
4600         }
4601       } else if ("attachments".equals(exp.getField())) {
4602         if (event.getAttachment() != null) {
4603           List<AttachmentResource> attRs = new LinkedList<AttachmentResource>();
4604           for (Attachment att : event.getAttachment()) {
4605             attRs.add(new AttachmentResource(att, basePath));
4606           }
4607           attRs = Utils.subList(attRs, exp.getOffset(), exp.getLimit());
4608           evtResource.setAtts(attRs.toArray(new AttachmentResource[attRs.size()]));
4609         }
4610       }
4611     }
4612 
4613     return extractObject(evtResource, fields);
4614   }
4615 
4616   private Object buildEventResource(CalendarEvent ev, UriInfo uriInfo, String expand, String fields) throws Exception {
4617     CalendarService service = calendarServiceInstance();
4618     String basePath = getBasePath(uriInfo);
4619     EventResource evtResource = new EventResource(ev, basePath);
4620 
4621     List<Expand> expands = Expand.parse(expand);
4622     for (Expand exp : expands) {
4623       if ("calendar".equals(exp.getField())) {
4624         Calendar cal = service.getCalendarById(ev.getCalendarId());
4625         setCalType(cal);
4626         evtResource.setCal(new CalendarResource(cal, basePath));
4627       } else if ("categories".equals(exp.getField())) {
4628         String categoryId = ev.getEventCategoryId();
4629         if (categoryId != null) {
4630           EventCategory evCat = service.getEventCategory(currentUserId(), categoryId);
4631           if (evCat != null) {
4632             CategoryResource[] catRs = new CategoryResource[] {new CategoryResource(evCat, basePath)};
4633             evtResource.setCats(Utils.<CategoryResource>subArray(catRs, exp.getOffset(), exp.getLimit()));
4634           }
4635         }
4636       } else if ("originalEvent".equals(exp.getField())) {
4637         String orgId = ev.getOriginalReference();
4638         if (orgId != null) {
4639           CalendarEvent orgEv = service.getEventById(orgId);
4640           if (orgEv != null) {
4641             evtResource.setOEvent(new EventResource(orgEv, basePath));
4642           }
4643         }
4644       } else if ("attachments".equals(exp.getField())) {
4645         if (ev.getAttachment() != null) {
4646           List<AttachmentResource> attRs = new LinkedList<AttachmentResource>();
4647           for (Attachment att : ev.getAttachment()) {
4648             attRs.add(new AttachmentResource(att, basePath));
4649           }
4650           attRs = Utils.subList(attRs, exp.getOffset(), exp.getLimit());
4651           evtResource.setAtts(attRs.toArray(new AttachmentResource[attRs.size()]));
4652         }
4653       }
4654     }
4655 
4656     return extractObject(evtResource, fields);
4657   }
4658 
4659   private Object buildFeedResource(FeedData feed, List<String> calIds, UriInfo uriInfo,
4660                                    String expand, String fields) throws Exception {
4661     CalendarService service = calendarServiceInstance();
4662     String basePath = getBasePath(uriInfo);
4663     FeedResource feedResource = new FeedResource(feed, calIds.toArray(new String[calIds.size()]), basePath);
4664 
4665     List<Expand> expands = Expand.parse(expand);
4666     for (Expand exp : expands) {
4667       if ("calendars".equals(exp.getField())) {
4668         List<Serializable> calendars = new ArrayList<Serializable>();
4669         for(String calId : Utils.subList(calIds, exp.getOffset(), exp.getLimit())) {
4670           Calendar cal = service.getCalendarById(calId);
4671           setCalType(cal);
4672           calendars.add(new CalendarResource(cal, getBasePath(uriInfo)));
4673         }
4674         feedResource.setCals(Utils.subList(calendars, exp.getOffset(), exp.getLimit()));
4675       }
4676     }
4677 
4678     return extractObject(feedResource, fields);
4679   }
4680 
4681   private Object buildInvitationResource(Invitation invitation,
4682                                          UriInfo uriInfo,
4683                                          String expand,
4684                                          String fields) throws Exception {
4685     CalendarService service = calendarServiceInstance();
4686     String basePath = getBasePath(uriInfo);
4687     InvitationResource ivtResource = new InvitationResource(invitation, basePath);
4688 
4689     List<Expand> expands = Expand.parse(expand);
4690     for (Expand exp : expands) {
4691       if ("event".equals(exp.getField())) {
4692         CalendarEvent event = service.getEventById(invitation.getEventId());
4693         ivtResource.setEvt(new EventResource(event, basePath));
4694       }
4695     }
4696 
4697     return extractObject(ivtResource, fields);
4698   }
4699 
4700   private String digest(byte[] data) throws Exception {
4701     MessageDigest md5 = MessageDigest.getInstance("MD5");
4702     byte[] hashCode = md5.digest(data);
4703     //Can't compile if return byte[] due to the bug from eXo rest framework
4704     return String.valueOf(hashCode);
4705   }
4706 
4707   private void setCalType(Calendar cal) {
4708     try {
4709       cal.setCalType(calendarServiceInstance().getTypeOfCalendar(currentUserId(), cal.getId()));
4710     } catch (Exception e) {
4711       log.error(e);
4712     }
4713   }
4714 
4715   private void saveEvent(int calType, CalendarEvent event, boolean bln) throws Exception {
4716     saveEvent(event.getCalendarId(), event.getCalendarId(), calType, calType, event, null, bln);
4717   }
4718 
4719   private void saveEvent(String fromCal, String toCal, int fromType, int toType, CalendarEvent event, RecurringUpdateType recurringUpdateType, boolean bln) throws Exception {
4720     CalendarService calService = calendarServiceInstance();
4721     if (recurringUpdateType == null) {
4722       //create new event or update non-repeat event
4723       if (bln) {
4724         switch (toType) {
4725           case Calendar.TYPE_PRIVATE:
4726             calService.saveUserEvent(currentUserId(), event.getCalendarId(), event, bln);
4727             break;
4728           case Calendar.TYPE_PUBLIC:
4729             calService.savePublicEvent(event.getCalendarId(), event, bln);
4730             break;
4731           case Calendar.TYPE_SHARED:
4732             calService.saveEventToSharedCalendar(currentUserId(), event.getCalendarId(), event,bln);
4733             break;
4734           default:
4735             break;
4736         }
4737       } else {
4738         List<CalendarEvent> listEvent = Arrays.asList(event);
4739         if (org.exoplatform.calendar.service.Utils.isExceptionOccurrence(event)) {
4740           calService.updateOccurrenceEvent(fromCal, toCal, String.valueOf(fromType), String.valueOf(toType), listEvent, currentUserId());
4741         } else {
4742           calService.moveEvent(fromCal, toCal, String.valueOf(fromType), String.valueOf(toType), listEvent, currentUserId()) ;
4743         }
4744       }
4745     } else {
4746       //update repeat event
4747       CalendarEvent original = calService.getRepetitiveEvent(event);
4748       recurringUpdateType = recurringUpdateType == null ? RecurringUpdateType.ONE : recurringUpdateType;
4749       switch (recurringUpdateType) {
4750         case ALL:
4751           calService.saveAllSeriesEvents(event, currentUserId());
4752           break;
4753         case FOLLOWING:
4754           calService.saveFollowingSeriesEvents(original, event, currentUserId());
4755           break;
4756         case ONE:
4757           calService.saveOneOccurrenceEvent(original, event, currentUserId());
4758           break;
4759       }
4760     }
4761   }
4762 
4763   public static class Expand {
4764     private String field;
4765     private int offset;
4766     private int limit;
4767 
4768     public Expand(String field, int offset, int limit) {
4769       this.field = field;
4770       this.offset = offset;
4771       this.limit = limit;
4772     }
4773 
4774     public static List<Expand> parse(String expand) {
4775       List<Expand> expands = new LinkedList<Expand>();
4776       
4777       if (expand != null) {
4778         String[] frags = expand.split(",");
4779         List<String> tmp = new LinkedList<String>();
4780         for (int i = 0; i < frags.length; i++) {
4781           String str = frags[i].trim();
4782           if (!str.contains("(") && str.contains("")) {
4783             tmp.add(str);
4784           } else if (str.contains("(") && i + 1 < frags.length) {
4785             tmp.add(str + "," + frags[++i]);
4786           }
4787         }
4788                 
4789         for (String exp : tmp) {        
4790           String fieldName = null;
4791           int offset = 0;
4792           int limit = -1;
4793           if (exp != null) {
4794             exp = exp.trim();
4795             int i =exp.indexOf('('); 
4796             if (i > 0) {
4797               fieldName = exp.substring(0, i).trim();
4798               try {
4799                 offset = Integer.parseInt(exp.substring(exp.indexOf("offset:") + "offset:".length(), exp.indexOf(",")).trim());
4800                 limit = Integer.parseInt(exp.substring(exp.indexOf("limit:") + "limit:".length(), exp.indexOf(")")).trim());
4801               } catch (Exception ex) {
4802                   log.debug(ex);
4803               }
4804             } else {
4805               fieldName = exp;
4806             }
4807           }
4808           
4809           expands.add(new Expand(fieldName, offset, limit));
4810         }      
4811       }
4812       
4813       return expands;
4814     }
4815     
4816     public String getField() {
4817       return field;
4818     }
4819     
4820     public int getOffset() {
4821       return offset;
4822     }
4823     
4824     public int getLimit() {
4825       return limit;
4826     }
4827   }
4828 }