/*
* � Copyright IBM Corp. 2012
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.ibm.domino.services.calendar.resources;
import static com.ibm.domino.commons.model.IGatekeeperProvider.FEATURE_REST_API_CALENDAR_EVENT_LIST;
import static com.ibm.domino.services.calendar.service.CalendarService.CALENDAR_SERVICE_LOGGER;
import static com.ibm.domino.services.calendar.service.CalendarService.FORMAT_ICALENDAR;
import static com.ibm.domino.services.calendar.service.CalendarService.MEDIA_TYPE_ICALENDAR;
import static com.ibm.domino.services.calendar.service.CalendarService.PATH_SEGMENT_SEPERATOR;
import static com.ibm.domino.services.calendar.service.CalendarService.STAT_CREATE_EVENT;
import static com.ibm.domino.services.calendar.service.CalendarService.STAT_VIEW_EVENTS;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_BEFORE;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_COUNT;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_DAYS;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_FIELDS;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_FORMAT;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_SINCE;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_SINCE_NOW;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_START;
import static com.ibm.domino.services.calendar.service.CalendarService.URL_PARAM_WORKFLOW;
import static com.ibm.domino.services.calendar.service.CalendarService.WORKSPACE_TITLE;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.data.CalendarParser;
import net.fortuna.ical4j.data.CalendarParserFactory;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.data.UnfoldingReader;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.util.CompatibilityHints;
import org.apache.wink.common.annotations.Workspace;
import com.ibm.commons.util.StringUtil;
import com.ibm.commons.util.io.json.JsonException;
import com.ibm.domino.calendar.store.EventSet;
import com.ibm.domino.calendar.store.StoreException;
import com.ibm.domino.commons.json.JsonIllegalValueException;
import com.ibm.domino.commons.util.UriHelper;
import com.ibm.domino.das.utils.ErrorHelper;
import com.ibm.domino.services.calendar.json.JsonCalendarGenerator;
import com.ibm.domino.services.calendar.json.JsonCalendarParser;
import com.ibm.domino.services.calendar.service.CalendarService;
import com.ibm.domino.services.calendar.store.StoreFactory;
import com.ibm.domino.services.calendar.util.Utils;
/**
* Event collection resource
*/
@Workspace(workspaceTitle = WORKSPACE_TITLE, collectionTitle = "Events") // $NON-NLS-1$
@Path("calendar/events") // $NON-NLS-1$
public class EventCollectionResource {
private static final String UNKNOWN_UID = "unknown"; // $NON-NLS-1$
private static final long ONE_DAY = 24L * 60 * 60 * 1000;
/**
* Gets events.
*
* @param linksBuilders
* @param uriInfo
* @param format
* @return
*/
@GET
public Response getEvents(@Context UriInfo uriInfo,
@QueryParam(URL_PARAM_FORMAT) String format,
@QueryParam(URL_PARAM_SINCE) String since,
@QueryParam(URL_PARAM_BEFORE) String before,
@QueryParam(URL_PARAM_COUNT) String count,
@QueryParam(URL_PARAM_START) String start,
@QueryParam(URL_PARAM_FIELDS) String fields,
@QueryParam(URL_PARAM_SINCE_NOW) String sincenow,
@QueryParam(URL_PARAM_DAYS) String days) {
CALENDAR_SERVICE_LOGGER.traceEntry(this, "getEvents"); // $NON-NLS-1$
CalendarService.beforeRequest(FEATURE_REST_API_CALENDAR_EVENT_LIST, STAT_VIEW_EVENTS);
String jsonEntity = null;
String eventString = null;
String contentRange = null;
Date fromDate = null;
Date toDate = null;
CalendarService.verifyDatabaseContext();
try {
// Validate parameters
if ( StringUtil.isNotEmpty(since) && StringUtil.isNotEmpty(sincenow) ) {
throw new WebApplicationException(CalendarService.createErrorResponse("A request cannot include both the \"since\" and \"sincenow\" parameters.", // $NLX-EventCollectionResource.Arequestcannotincludeboththesince-1$
Response.Status.BAD_REQUEST));
}
if ( StringUtil.isNotEmpty(before) && StringUtil.isNotEmpty(days) ) {
throw new WebApplicationException(CalendarService.createErrorResponse("A request cannot include both the \"before\" and \"days\" parameters.", // $NLX-EventCollectionResource.Arequestcannotincludeboththebefor-1$
Response.Status.BAD_REQUEST));
}
// Get date range (if any)
Date now = new Date();
if ( StringUtil.isNotEmpty(since) ) {
fromDate = Utils.dateFromString(since);
}
else if ( StringUtil.isNotEmpty(sincenow) ) {
int iSinceNow = CalendarService.getParameterInt(URL_PARAM_SINCE_NOW, sincenow, false);
fromDate = new Date(now.getTime() + (ONE_DAY * iSinceNow));
}
if ( StringUtil.isNotEmpty(before) ) {
toDate = Utils.dateFromString(before);
if ( fromDate != null && (toDate.getTime() <= fromDate.getTime()) ) {
// Before date cannot be less than since date
throw new WebApplicationException(CalendarService.createErrorResponse(
"The \"before\" date must be after the \"since\" date", // $NLX-EventCollectionResource.Beforedatecannotbelessthanorequal-1$
Response.Status.BAD_REQUEST));
}
}
else if ( StringUtil.isNotEmpty(days) ) {
int iDays = CalendarService.getParameterInt(URL_PARAM_DAYS, days, false);
if ( iDays <= 0 ) {
throw new WebApplicationException(CalendarService.createErrorResponse(
MessageFormat.format("Invalid \"days\" parameter: {0}. The value must be greater than 0.", iDays), // $NLX-EventCollectionResource.Invaliddaysparameter0Itmustbegrea-1$
Response.Status.BAD_REQUEST));
}
if ( fromDate == null ) {
fromDate = now;
}
toDate = new Date(fromDate.getTime() + (ONE_DAY * iDays));
}
// Parse paging parameters
int iStart = 0;
int iCount = 50;
if ( StringUtil.isNotEmpty(count) ) {
iCount = CalendarService.getParameterInt(URL_PARAM_COUNT, count, false);
}
if ( StringUtil.isNotEmpty(start) ) {
iStart = CalendarService.getParameterInt(URL_PARAM_START, start, false);
}
if ( iStart < 0 ) {
throw new WebApplicationException(CalendarService.createErrorResponse(
MessageFormat.format("Invalid start parameter: {0}. It must be greater than or equal to 0.", iStart), // $NLX-EventCollectionResource.Invalidstartparameter0Itmustbegre-1$
Response.Status.BAD_REQUEST));
}
if ( iCount < 1 || iCount > 100 ) {
throw new WebApplicationException(CalendarService.createErrorResponse(
MessageFormat.format("Invalid count parameter: {0}. It must be greater than 0 and less than 101.", iCount), // $NLX-EventCollectionResource.Invalidcountparameter0Itmustbegre-1$
Response.Status.BAD_REQUEST));
}
ArrayList<String> fieldList = null;
if ( StringUtil.isNotEmpty(fields) ) {
fieldList = getParameterStringList(URL_PARAM_FIELDS, fields);
}
// Get the events in iCalendar format
EventSet set = StoreFactory.getEventStore().getEvents(fromDate, toDate, iStart, iCount, fieldList);
if ( set != null && !StringUtil.isEmpty(set.getEvents())) {
eventString = set.getEvents();
}
if ( eventString != null ) {
// Set the content range header
if ( set.getEventCount() > 0 ) {
int iLast = iStart + set.getEventCount() - 1;
contentRange = "items " + iStart + "-" + iLast; // $NON-NLS-1$
}
// Convert to alternate format (if necessary)
if ( ! FORMAT_ICALENDAR.equalsIgnoreCase(format) ) {
jsonEntity = createJsonArray(eventString, uriInfo);
}
}
}
catch(StoreException e) {
throw new WebApplicationException(ErrorHelper.createErrorResponse(e, Utils.mapStatus(e), Utils.getExtraProps(e)));
}
catch(ParserException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch(IOException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch (ParseException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.BAD_REQUEST));
}
ResponseBuilder builder = Response.ok();
if ( contentRange != null ) {
builder.header("Content-Range", contentRange); // $NON-NLS-1$
}
// If there are no events in the range, we return HTTP 200, no Content-Type, and
// no entity body. After reading about best practices, I'm sure the HTTP 200 is right.
// However, it might be better to return an an empty object in JSON or iCalendar format.
if ( jsonEntity != null ) {
builder.type(MediaType.APPLICATION_JSON_TYPE).entity(jsonEntity);
}
else if ( eventString != null ) {
builder.type(MEDIA_TYPE_ICALENDAR).entity(eventString);
}
Response response = builder.build();
CALENDAR_SERVICE_LOGGER.traceExit(this, "getEvents", response); // $NON-NLS-1$
return response;
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createJsonEvent(String requestEntity, @Context UriInfo uriInfo,
@QueryParam(URL_PARAM_WORKFLOW) String workflow) {
CALENDAR_SERVICE_LOGGER.traceEntry(this, "createJsonEvent"); // $NON-NLS-1$
CalendarService.beforeRequest(FEATURE_REST_API_CALENDAR_EVENT_LIST, STAT_CREATE_EVENT);
CalendarService.verifyDatabaseContext();
String responseEntity = null;
URI location = null;
try {
if ( StringUtil.isEmpty(requestEntity) ) {
throw new WebApplicationException(CalendarService.createErrorResponse("Empty request body.", Status.BAD_REQUEST)); // $NLX-EventCollectionResource.Emptyrequestbody-1$
}
// Parse JSON to iCal4j object model
StringReader reader = new StringReader(requestEntity);
JsonCalendarParser parser = new JsonCalendarParser();
Calendar calendar = parser.parse(reader);
// Serialize to iCalendar format
ByteArrayOutputStream baos = new ByteArrayOutputStream();
CalendarOutputter outputter = new CalendarOutputter(false);
outputter.output(calendar, baos);
String icalendar = baos.toString("UTF-8");//$NON-NLS-1$
// Store new event
int flags = EventResource.getFlags(workflow);
String output = StoreFactory.getEventStore().createEvent(icalendar, flags);
// Extract the UID from the new event
String uid = extractUid(output);
if ( uid == null ) {
uid = UNKNOWN_UID;
}
// Set the Location header for the response
String url = uriInfo.getAbsolutePath().toString() + PATH_SEGMENT_SEPERATOR + uid;
location = new URI(url);
location = CalendarService.adaptUriToScn(location);
// Prepare response
StringReader sr = new StringReader(output);
StringBuilder sb = new StringBuilder();
CalendarParser cp = CalendarParserFactory.getInstance().createParser();
URI baseURI;
try {
baseURI = UriHelper.copy(uriInfo.getAbsolutePath(),CalendarService.isUseRelativeUrls());
baseURI = CalendarService.adaptUriToScn(baseURI);
}
catch(IllegalArgumentException e){
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.BAD_REQUEST));
}
cp.parse(new UnfoldingReader(sr, true), new JsonCalendarGenerator(sb, baseURI, false));
responseEntity = sb.toString();
}
catch(JsonIllegalValueException e){
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.BAD_REQUEST));
}
catch (JsonException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.BAD_REQUEST));
}
catch (IOException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch (ValidationException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch (StoreException e) {
throw new WebApplicationException(ErrorHelper.createErrorResponse(e, Utils.mapStatus(e), Utils.getExtraProps(e)));
}
catch (ParserException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch (URISyntaxException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
ResponseBuilder builder = Response.created(location);
builder.type(MediaType.APPLICATION_JSON).entity(responseEntity);
Response response = builder.build();
CALENDAR_SERVICE_LOGGER.traceExit(this, "createJsonEvent", response); // $NON-NLS-1$
return response;
}
/**
* Creates an event.
*
* @param requestEntity
* @param uriInfo
* @return
*/
@POST
@Consumes(MEDIA_TYPE_ICALENDAR)
public Response createEvent(String requestEntity, @Context UriInfo uriInfo,
@QueryParam(URL_PARAM_WORKFLOW) String workflow) {
CALENDAR_SERVICE_LOGGER.traceEntry(this, "createEvent"); // $NON-NLS-1$
CalendarService.beforeRequest(FEATURE_REST_API_CALENDAR_EVENT_LIST, STAT_CREATE_EVENT);
CalendarService.verifyDatabaseContext();
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
String responseEntity = null;
URI location = null;
try {
if ( StringUtil.isEmpty(requestEntity) ) {
throw new WebApplicationException(CalendarService.createErrorResponse("Empty request body.", Status.BAD_REQUEST)); // $NLX-EventCollectionResource.Emptyrequestbody.1-1$
}
// Create the event
int flags = EventResource.getFlags(workflow);
responseEntity = StoreFactory.getEventStore().createEvent(requestEntity, flags);
// Extract the UID from the new event
String uid = extractUid(responseEntity);
if ( uid == null ) {
uid = UNKNOWN_UID;
}
// Set the Location header for the response
String url = uriInfo.getAbsolutePath().toString() + PATH_SEGMENT_SEPERATOR + uid;
location = new URI(url);
location = CalendarService.adaptUriToScn(location);
}
catch(StoreException e) {
throw new WebApplicationException(ErrorHelper.createErrorResponse(e, Utils.mapStatus(e), Utils.getExtraProps(e)));
}
catch(URISyntaxException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch (IOException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
catch (ParserException e) {
throw new WebApplicationException(CalendarService.createErrorResponse(e, Response.Status.INTERNAL_SERVER_ERROR));
}
finally {
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, false);
}
ResponseBuilder builder = Response.created(location);
builder.type(MEDIA_TYPE_ICALENDAR).entity(responseEntity);
Response response = builder.build();
CALENDAR_SERVICE_LOGGER.traceExit(this, "createEvent", response); // $NON-NLS-1$
return response;
}
private String extractUid(String icalendar) throws IOException, ParserException {
String uid = null;
StringReader reader = new StringReader(icalendar);
CalendarBuilder builder = new CalendarBuilder();
Calendar calendar = builder.build(new UnfoldingReader(reader, true));
if ( calendar != null && calendar.getComponents() != null ) {
Iterator<Component> iterator = calendar.getComponents().iterator();
while (iterator.hasNext()) {
Component component = iterator.next();
if ( component instanceof VEvent ) {
uid = ((VEvent)component).getUid().getValue();
break;
}
}
}
return uid;
}
/**
* Formats a list of events as a JSON array.
*
* @param events
* @param uriInfo
* @return
* @throws JsonException
* @throws IOException
*/
private String createJsonArray(String eventString, UriInfo uriInfo)
throws ParserException, IOException {
StringReader reader = new StringReader(eventString);
StringBuilder sb = new StringBuilder();
CalendarParser parser = CalendarParserFactory.getInstance().createParser();
URI baseURI = UriHelper.copy(uriInfo.getAbsolutePath(),CalendarService.isUseRelativeUrls());
baseURI = CalendarService.adaptUriToScn(baseURI);
parser.parse(new UnfoldingReader(reader, true), new JsonCalendarGenerator(sb, baseURI, false));
return sb.toString();
}
// parameter should like: fields=start,end
protected ArrayList<String> getParameterStringList(String paramName, String paramValue) {
// check the format of parameter list
Pattern pattern = Pattern.compile("(\\s*[\\w-]+\\s*,)*\\s*([\\w-]+)\\s*"); // $NON-NLS-1$
Matcher matcher = pattern.matcher(paramValue);
if(!matcher.matches()){
String msg = MessageFormat.format("Invalid {0} parameter: {1} ", paramName, paramValue); // $NLX-EventCollectionResource.Invalid0parameter1-1$
throw new WebApplicationException(
CalendarService.createErrorResponse(
new Exception(msg), Response.Status.BAD_REQUEST));
}
// if match, at last have one field
ArrayList<String> vaueList = new ArrayList<String>();
String[] valueArray = paramValue.split(",");
for(int i = 0; i< valueArray.length; i++){
vaueList.add(valueArray[i].trim());
}
return vaueList;
}
}