org.opencastproject.adminui.endpoint.AbstractEventEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.adminui.endpoint.AbstractEventEndpoint.java

Source

/**
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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 org.opencastproject.adminui.endpoint;

import static com.entwinemedia.fn.data.json.Jsons.a;
import static com.entwinemedia.fn.data.json.Jsons.f;
import static com.entwinemedia.fn.data.json.Jsons.j;
import static com.entwinemedia.fn.data.json.Jsons.jsonArrayFromList;
import static com.entwinemedia.fn.data.json.Jsons.v;
import static com.entwinemedia.fn.data.json.Jsons.vN;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.apache.commons.lang.StringUtils.trimToNull;
import static org.opencastproject.index.service.util.RestUtils.conflictJson;
import static org.opencastproject.index.service.util.RestUtils.notFound;
import static org.opencastproject.index.service.util.RestUtils.okJson;
import static org.opencastproject.index.service.util.RestUtils.okJsonList;
import static org.opencastproject.util.Jsons.arr;
import static org.opencastproject.util.Jsons.obj;
import static org.opencastproject.util.Jsons.p;
import static org.opencastproject.util.RestUtil.splitCommaSeparatedParam;
import static org.opencastproject.util.RestUtil.R.badRequest;
import static org.opencastproject.util.RestUtil.R.forbidden;
import static org.opencastproject.util.RestUtil.R.notFound;
import static org.opencastproject.util.RestUtil.R.ok;
import static org.opencastproject.util.RestUtil.R.serverError;
import static org.opencastproject.util.data.Monadics.mlist;
import static org.opencastproject.util.data.Option.option;
import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;

import org.opencastproject.adminui.exception.JobEndpointException;
import org.opencastproject.adminui.impl.AdminUIConfiguration;
import org.opencastproject.adminui.impl.index.AdminUISearchIndex;
import org.opencastproject.adminui.util.ParticipationUtils;
import org.opencastproject.archive.api.ArchiveException;
import org.opencastproject.archive.api.HttpMediaPackageElementProvider;
import org.opencastproject.archive.opencast.OpencastArchive;
import org.opencastproject.archive.opencast.OpencastQueryBuilder;
import org.opencastproject.archive.opencast.OpencastResultSet;
import org.opencastproject.authorization.xacml.manager.api.AclService;
import org.opencastproject.authorization.xacml.manager.api.AclServiceException;
import org.opencastproject.authorization.xacml.manager.api.EpisodeACLTransition;
import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
import org.opencastproject.authorization.xacml.manager.api.TransitionQuery;
import org.opencastproject.capture.admin.api.Agent;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.comments.Comment;
import org.opencastproject.comments.CommentException;
import org.opencastproject.comments.CommentParser;
import org.opencastproject.comments.CommentReply;
import org.opencastproject.comments.events.EventCommentService;
import org.opencastproject.index.service.api.IndexService;
import org.opencastproject.index.service.api.IndexService.Source;
import org.opencastproject.index.service.catalog.adapter.AbstractMetadataCollection;
import org.opencastproject.index.service.catalog.adapter.MetadataField;
import org.opencastproject.index.service.catalog.adapter.MetadataList;
import org.opencastproject.index.service.catalog.adapter.MetadataList.Locked;
import org.opencastproject.index.service.catalog.adapter.MetadataUtils;
import org.opencastproject.index.service.catalog.adapter.events.EventCatalogUIAdapter;
import org.opencastproject.index.service.exception.InternalServerErrorException;
import org.opencastproject.index.service.impl.index.event.Event;
import org.opencastproject.index.service.impl.index.event.EventIndexSchema;
import org.opencastproject.index.service.impl.index.event.EventSearchQuery;
import org.opencastproject.index.service.resources.list.api.ListProvidersService;
import org.opencastproject.index.service.resources.list.provider.CommentsListProvider.RESOLUTION;
import org.opencastproject.index.service.resources.list.provider.EventsListProvider.Comments;
import org.opencastproject.index.service.resources.list.query.EventListQuery;
import org.opencastproject.index.service.util.AccessInformationUtil;
import org.opencastproject.index.service.util.JSONUtils;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.ingest.api.IngestService;
import org.opencastproject.matterhorn.search.SearchIndexException;
import org.opencastproject.matterhorn.search.SearchResult;
import org.opencastproject.matterhorn.search.SearchResultItem;
import org.opencastproject.matterhorn.search.SortCriterion;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.AudioStream;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement.Type;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.Publication;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.mediapackage.VideoStream;
import org.opencastproject.mediapackage.track.AudioStreamImpl;
import org.opencastproject.mediapackage.track.VideoStreamImpl;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogList;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
import org.opencastproject.pm.api.Message;
import org.opencastproject.pm.api.Person;
import org.opencastproject.pm.api.Recording;
import org.opencastproject.pm.api.persistence.ParticipationManagementDatabase;
import org.opencastproject.pm.api.persistence.ParticipationManagementDatabase.SortType;
import org.opencastproject.pm.api.persistence.ParticipationManagementDatabaseException;
import org.opencastproject.rest.BulkOperationResult;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.AclScope;
import org.opencastproject.security.api.AuthorizationService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.api.User;
import org.opencastproject.security.util.SecurityContext;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.systems.MatterhornConstants;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.Jsons.Val;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.RestUtil;
import org.opencastproject.util.UrlSupport;
import org.opencastproject.util.data.Effect0;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.Monadics;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.opencastproject.workflow.api.ConfiguredWorkflowRef;
import org.opencastproject.workflow.api.WorkflowDatabaseException;
import org.opencastproject.workflow.api.WorkflowDefinition;
import org.opencastproject.workflow.api.WorkflowException;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState;
import org.opencastproject.workflow.api.WorkflowQuery;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workflow.api.WorkflowSet;
import org.opencastproject.workflow.api.WorkflowUtil;
import org.opencastproject.workflow.handler.distribution.EngagePublicationChannel;
import org.opencastproject.workflow.handler.distribution.PublishInternalWorkflowOperationHandler;
import org.opencastproject.workspace.api.Workspace;

import com.entwinemedia.fn.Fn;
import com.entwinemedia.fn.Stream;
import com.entwinemedia.fn.data.Opt;
import com.entwinemedia.fn.data.json.JField;
import com.entwinemedia.fn.data.json.JObjectWrite;
import com.entwinemedia.fn.data.json.JValue;
import com.entwinemedia.fn.data.json.Jsons;

import net.fortuna.ical4j.model.property.RRule;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.http.HttpStatus;
import org.codehaus.jettison.json.JSONException;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.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.Status;

/**
 * The event endpoint acts as a facade for WorkflowService and Archive providing a unified query interface and result
 * set.
 * <p/>
 * This first implementation uses the {@link org.opencastproject.archive.opencast.OpencastArchive}. In a later iteration
 * the endpoint may abstract over the concrete archive.
 */
@Path("/")
@RestService(name = "eventservice", title = "Event Service", notes = "", abstractText = "Provides resources and operations related to the events")
public abstract class AbstractEventEndpoint {

    /** The logging facility */
    static final Logger logger = LoggerFactory.getLogger(AbstractEventEndpoint.class);

    private static final int CREATED_BY_UI_ORDER = 14;

    public abstract WorkflowService getWorkflowService();

    public abstract AdminUISearchIndex getIndex();

    public abstract DublinCoreCatalogService getDublinCoreService();

    public abstract JobEndpoint getJobService();

    public abstract ListProvidersService getListProviderService();

    public abstract OpencastArchive getArchive();

    /** A media package element provider used by the archive. */
    public abstract HttpMediaPackageElementProvider getHttpMediaPackageElementProvider();

    public abstract Workspace getWorkspace();

    public abstract AclService getAclService();

    public abstract SeriesService getSeriesService();

    public abstract ParticipationManagementDatabase getPMPersistence();

    public abstract EventCommentService getEventCommentService();

    public abstract SecurityService getSecurityService();

    public abstract IndexService getIndexService();

    public abstract IngestService getIngestService();

    public abstract AuthorizationService getAuthorizationService();

    public abstract SchedulerService getSchedulerService();

    public abstract CaptureAgentStateService getCaptureAgentStateService();

    public abstract List<EventCatalogUIAdapter> getEventCatalogUIAdapters(String organization);

    public abstract EventCatalogUIAdapter getEpisodeCatalogUIAdapter();

    public abstract AdminUIConfiguration getAdminUIConfiguration();

    /** Default server URL */
    protected String serverUrl = "http://localhost:8080";

    /** Service url */
    protected String serviceUrl = null;

    /** A parser for handling JSON documents inside the body of a request. **/
    private final JSONParser parser = new JSONParser();

    /** The single thread executor service */
    private ExecutorService executorService = Executors.newSingleThreadExecutor();

    private static final Map<String, String> PUBLICATION_CHANNELS = new HashMap<String, String>();

    static {
        PUBLICATION_CHANNELS.put(EngagePublicationChannel.CHANNEL_ID, "EVENTS.EVENTS.DETAILS.GENERAL.ENGAGE");
        PUBLICATION_CHANNELS.put("youtube", "EVENTS.EVENTS.DETAILS.GENERAL.YOUTUBE");
    }

    /**
     * Activates REST service.
     *
     * @param cc
     *          ComponentContext
     */
    public void activate(ComponentContext cc) {
        if (cc != null) {
            String ccServerUrl = cc.getBundleContext().getProperty(MatterhornConstants.SERVER_URL_PROPERTY);
            if (StringUtils.isNotBlank(ccServerUrl))
                this.serverUrl = ccServerUrl;
        }
        serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
    }

    @GET
    @Path("catalogAdapters")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "getcataloguiadapters", description = "Returns the available catalog UI adapters as JSON", returnDescription = "The catalog UI adapters as JSON", reponses = {
            @RestResponse(description = "Returns the available catalog UI adapters as JSON", responseCode = HttpServletResponse.SC_OK) })
    public Response getCatalogAdapters() {
        List<JValue> adapters = new ArrayList<JValue>();
        for (EventCatalogUIAdapter adapter : getEventCatalogUIAdapters()) {
            List<JField> fields = new ArrayList<JField>();
            fields.add(f("flavor", v(adapter.getFlavor().toString())));
            fields.add(f("title", v(adapter.getUITitle())));
            adapters.add(j(fields));
        }
        return okJson(a(adapters));
    }

    @GET
    @Path("{eventId}")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "getevent", description = "Returns the event by the given id as JSON", returnDescription = "The event as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns the event as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventResponse(@PathParam("eventId") String id) throws Exception {
        for (final Event event : getEvent(id)) {
            return okJson(eventToJSON(event));
        }
        return notFound("Cannot find an event with id '%s'.", id);
    }

    @DELETE
    @Path("{eventId}")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "deleteevent", description = "Delete a single event.", returnDescription = "Ok if the event has been deleted.", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The id of the event to delete.", type = STRING), }, reponses = {
                    @RestResponse(responseCode = SC_OK, description = "The event has been deleted."),
                    @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The event could not be found."),
                    @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") })
    public Response deleteEvent(@PathParam("eventId") String id) throws NotFoundException, UnauthorizedException {
        if (!removeEvent(id))
            return Response.serverError().build();

        return Response.ok().build();
    }

    @POST
    @Path("deleteEvents")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "deleteevents", description = "Deletes a json list of events by their given ids e.g. [\"1dbe7255-e17d-4279-811d-a5c7ced689bf\", \"04fae22b-0717-4f59-8b72-5f824f76d529\"]", returnDescription = "Returns a JSON object containing a list of event ids that were deleted, not found or if there was a server error.", reponses = {
            @RestResponse(description = "Events have been deleted", responseCode = HttpServletResponse.SC_OK),
            @RestResponse(description = "The list of ids could not be parsed into a json list.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
            @RestResponse(description = "If the current user is not authorized to perform this action", responseCode = HttpServletResponse.SC_UNAUTHORIZED) })
    public Response deleteEvents(String eventIdsContent) throws UnauthorizedException {
        if (StringUtils.isBlank(eventIdsContent)) {
            return Response.status(Response.Status.BAD_REQUEST).build();
        }

        JSONArray eventIdsJsonArray;
        try {
            eventIdsJsonArray = (JSONArray) parser.parse(eventIdsContent);
        } catch (org.json.simple.parser.ParseException e) {
            logger.error("Unable to parse '{}' because: {}", eventIdsContent, ExceptionUtils.getStackTrace(e));
            return Response.status(Response.Status.BAD_REQUEST).build();
        } catch (ClassCastException e) {
            logger.error("Unable to cast '{}' because: {}", eventIdsContent, ExceptionUtils.getStackTrace(e));
            return Response.status(Response.Status.BAD_REQUEST).build();
        }

        BulkOperationResult result = new BulkOperationResult();

        for (Object eventIdObject : eventIdsJsonArray) {
            String eventId = eventIdObject.toString();
            try {
                if (!removeEvent(eventId)) {
                    result.addServerError(eventId);
                } else {
                    result.addOk(eventId);
                }
            } catch (NotFoundException e) {
                result.addNotFound(eventId);
            }
        }
        return Response.ok(result.toJson()).build();
    }

    /**
     * Removes an event.
     *
     * @param id
     *          The id for the event to remove.
     * @return true if the event was found and removed.
     */
    private boolean removeEvent(String id) throws NotFoundException, UnauthorizedException {
        boolean unauthorizedScheduler = false;
        boolean notFoundScheduler = false;
        boolean removedScheduler = true;
        try {
            getSchedulerService().removeEvent(getSchedulerService().getEventId(id));
        } catch (NotFoundException e) {
            notFoundScheduler = true;
        } catch (SchedulerException e) {
            removedScheduler = false;
            logger.error("Unable to remove the event '{}' from scheduler service: {}", id,
                    ExceptionUtils.getStackTrace(e));
        } catch (UnauthorizedException e) {
            unauthorizedScheduler = true;
        }

        boolean unauthorizedWorkflow = false;
        boolean notFoundWorkflow = false;
        boolean removedWorkflow = true;
        try {
            WorkflowQuery workflowQuery = new WorkflowQuery().withMediaPackage(id);
            WorkflowSet workflowSet = getWorkflowService().getWorkflowInstances(workflowQuery);
            if (workflowSet.size() == 0)
                notFoundWorkflow = true;
            for (WorkflowInstance instance : workflowSet.getItems()) {
                getWorkflowService().stop(instance.getId());
                getWorkflowService().remove(instance.getId());
            }
        } catch (NotFoundException e) {
            notFoundWorkflow = true;
        } catch (WorkflowDatabaseException e) {
            removedWorkflow = false;
            logger.error("Unable to remove the event '{}' because removing workflow failed: {}", id,
                    ExceptionUtils.getStackTrace(e));
        } catch (WorkflowException e) {
            removedWorkflow = false;
            logger.error("Unable to remove the event '{}' because removing workflow failed: {}", id,
                    ExceptionUtils.getStackTrace(e));
        } catch (UnauthorizedException e) {
            unauthorizedWorkflow = true;
        }

        boolean unauthorizedArchive = false;
        boolean notFoundArchive = false;
        boolean removedArchive = true;
        try {
            OpencastResultSet archiveRes = getArchive().find(
                    OpencastQueryBuilder.query().mediaPackageId(id).onlyLastVersion(true),
                    getHttpMediaPackageElementProvider().getUriRewriter());
            if (archiveRes.size() > 0) {
                getArchive().delete(id);
            } else {
                notFoundArchive = true;
            }
        } catch (ArchiveException e) {
            if (e.isCauseNotAuthorized()) {
                unauthorizedArchive = true;
            } else if (e.isCauseNotFound()) {
                notFoundArchive = true;
            } else {
                removedArchive = false;
                logger.error("Unable to remove the event '{}' from the archive: {}", id,
                        ExceptionUtils.getStackTrace(e));
            }
        }

        if (notFoundScheduler && notFoundWorkflow && notFoundArchive)
            throw new NotFoundException("Event id " + id + " not found.");

        if (unauthorizedScheduler || unauthorizedWorkflow || unauthorizedArchive)
            throw new UnauthorizedException("Not authorized to remove event id " + id);

        return removedScheduler && removedWorkflow && removedArchive;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("events/recipients")
    @RestQuery(name = "geteventsrecipients", description = "Returns the events recipients as JSON", returnDescription = "Returns the events recipients as JSON", restParameters = {
            @RestParameter(name = "eventIds", isRequired = true, description = "A list of comma separated event IDs", type = STRING), }, reponses = {
                    @RestResponse(responseCode = SC_OK, description = "The events recipients as JSON."),
                    @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "At least one event id must be set.") })
    public Response getEventsRecordingsAndRecipients(@QueryParam("eventIds") String eventIds) {
        if (getPMPersistence() == null)
            return Response.status(Status.SERVICE_UNAVAILABLE).build();

        final Monadics.ListMonadic<String> eIds = splitCommaSeparatedParam(option(eventIds));
        if (eIds.value().isEmpty())
            return badRequest();

        List<Recording> recordings = ParticipationUtils.getRecordingsByEventId(getSchedulerService(),
                getPMPersistence(), eIds.value());
        List<Val> recipientsArr = mlist(new HashSet<Person>(mlist(recordings).flatMap(getRecipients).value()))
                .map(JSONUtils.personToJsonVal).value();
        return Response.ok(obj(p("recipients", arr(recipientsArr))).toJson()).build();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{eventId}/messages")
    @RestQuery(name = "geteventmessages", description = "Returns the event messages as JSON", returnDescription = "Returns the event messages as JSON", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = STRING) }, restParameters = {
                    @RestParameter(name = "sort", isRequired = false, description = "The sort order.  May include any of the following: DATE OR SENDER.  Add '_DESC' to reverse the sort order (e.g. DATE_DESC).", type = STRING) }, reponses = {
                            @RestResponse(responseCode = SC_OK, description = "The event messages as JSON."),
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "The event has not been found"),
                            @RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid SORT type, it was not DATE, DATE_DESC SENDER or SENDER_DESC"),
                            @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") })
    public Response getRecordingMessages(@PathParam("eventId") String eventId, @QueryParam("sort") String sort)
            throws NotFoundException {
        if (getPMPersistence() == null)
            return Response.status(Status.SERVICE_UNAVAILABLE).build();

        Option<SortType> sortType = Option.<SortType>none();
        sortType = ParticipationUtils.getMessagesSortField(sort);
        if (StringUtils.isNotBlank(sort) && sortType.isNone()) {
            return Response.status(SC_BAD_REQUEST).build();
        }

        Long scheduledEventId;
        try {
            scheduledEventId = getSchedulerService().getEventId(eventId);
        } catch (SchedulerException e) {
            logger.error("Unable to get scheduled event id by event {}: {}", eventId,
                    ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }

        try {
            Long recordingId = getPMPersistence().getRecordingByEvent(scheduledEventId).getId().getOrElse(-1L);
            List<Val> jsonArr = new ArrayList<org.opencastproject.util.Jsons.Val>();
            for (Message m : getPMPersistence().getMessagesByRecordingId(recordingId, sortType)) {
                jsonArr.add(m.toJson());
            }
            return Response.ok(org.opencastproject.util.Jsons.arr(jsonArr).toJson()).build();
        } catch (ParticipationManagementDatabaseException e) {
            logger.error("Unable to get messages by recording {}: {}", eventId, ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @GET
    @Path("{eventId}/general.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventgeneral", description = "Returns all the data related to the general tab in the event details modal as JSON", returnDescription = "All the data related to the event general tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id (mediapackage id).", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event general tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventGeneralTab(@PathParam("eventId") String id) throws Exception {
        Opt<Event> optEvent = getEvent(id);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", id);

        // Quick actions have been temporally removed from the general tab
        // ---------------------------------------------------------------
        // List<JValue> actions = new ArrayList<JValue>();
        // List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions();
        // for (WorkflowDefinition wflDef : workflowsDefinitions) {
        // if (wflDef.containsTag(WORKFLOWDEF_TAG)) {
        //
        // actions.add(j(f("id", v(wflDef.getId())), f("title", v(Opt.nul(wflDef.getTitle()).or(""))),
        // f("description", v(Opt.nul(wflDef.getDescription()).or(""))),
        // f("configuration_panel", v(Opt.nul(wflDef.getConfigurationPanel()).or("")))));
        // }
        // }

        Event event = optEvent.get();
        Opt<MediaPackage> mpOpt = getIndexService().getEventMediapackage(event);
        List<JValue> pubJSON = new ArrayList<JValue>();
        if (mpOpt.isSome()) {
            for (JObjectWrite json : Stream.$(mpOpt.get().getPublications()).filter(internalChannelFilter)
                    .map(publicationToJson)) {
                pubJSON.add(json);
            }
        }

        return okJson(j(f("publications", a(pubJSON)), f("optout", vN(event.getOptedOut())),
                f("blacklisted", vN(event.getBlacklisted())), f("review-status", vN(event.getReviewStatus()))));
    }

    @GET
    @Path("{eventId}/comments")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventcomments", description = "Returns all the data related to the comments tab in the event details modal as JSON", returnDescription = "All the data related to the event comments tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event comments tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventComments(@PathParam("eventId") String eventId) throws Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            List<Comment> comments = getEventCommentService().getComments(eventId);
            List<Val> commentArr = new ArrayList<Val>();
            for (Comment c : comments) {
                commentArr.add(c.toJson());
            }
            return Response
                    .ok(org.opencastproject.util.Jsons.arr(commentArr).toJson(), MediaType.APPLICATION_JSON_TYPE)
                    .build();
        } catch (CommentException e) {
            logger.error("Unable to get comments from event {}: {}", eventId, ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{eventId}/comment/{commentId}")
    @RestQuery(name = "geteventcomment", description = "Returns the comment with the given identifier", returnDescription = "Returns the comment as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, reponses = {
                    @RestResponse(responseCode = SC_OK, description = "The comment as JSON."),
                    @RestResponse(responseCode = SC_NOT_FOUND, description = "No event or comment with this identifier was found.") })
    public Response getEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId)
            throws NotFoundException, Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            Comment comment = getEventCommentService().getComment(eventId, commentId);
            return Response.ok(comment.toJson().toJson()).build();
        } catch (NotFoundException e) {
            throw e;
        } catch (Exception e) {
            logger.error("Could not retrieve comment {}: {}", commentId, ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @PUT
    @Path("{eventId}/comment/{commentId}")
    @RestQuery(name = "updateeventcomment", description = "Updates an event comment", returnDescription = "The updated comment as JSON.", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, restParameters = {
                    @RestParameter(name = "text", isRequired = false, description = "The comment text", type = TEXT),
                    @RestParameter(name = "reason", isRequired = false, description = "The comment reason", type = STRING),
                    @RestParameter(name = "resolved", isRequired = false, description = "The comment resolved status", type = RestParameter.Type.BOOLEAN) }, reponses = {
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to update has not been found."),
                            @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
    public Response updateEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId,
            @FormParam("text") String text, @FormParam("reason") String reason,
            @FormParam("resolved") Boolean resolved) throws NotFoundException, Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            Comment dto = getEventCommentService().getComment(eventId, commentId);

            if (StringUtils.isNotBlank(text)) {
                text = text.trim();
            } else {
                text = dto.getText();
            }

            if (StringUtils.isNotBlank(reason)) {
                reason = reason.trim();
            } else {
                reason = dto.getReason();
            }

            if (resolved == null)
                resolved = dto.isResolvedStatus();

            Comment updatedComment = Comment.create(dto.getId(), text, dto.getAuthor(), reason, resolved,
                    dto.getCreationDate(), new Date(), dto.getReplies());

            updatedComment = getEventCommentService().updateComment(eventId, updatedComment);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.ok(updatedComment.toJson().toJson()).build();
        } catch (NotFoundException e) {
            throw e;
        } catch (Exception e) {
            logger.error("Unable to update the comments catalog on event {}: {}", eventId,
                    ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @POST
    @Path("{eventId}/access")
    @RestQuery(name = "applyAclToEvent", description = "Immediate application of an ACL to an event", returnDescription = "Status code", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The event ID", type = STRING) }, restParameters = {
                    @RestParameter(name = "acl", isRequired = true, description = "The ACL to apply", type = STRING) }, reponses = {
                            @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"),
                            @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the given ACL"),
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "The the event has not been found"),
                            @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error") })
    public Response applyAclToEvent(@PathParam("eventId") String eventId, @FormParam("acl") String acl)
            throws NotFoundException, InternalServerErrorException {
        final AccessControlList accessControlList;
        try {
            accessControlList = AccessControlParser.parseAcl(acl);
        } catch (Exception e) {
            logger.warn("Unable to parse ACL '{}'", acl);
            return badRequest();
        }

        try {
            final Opt<Event> optEvent = getEvent(eventId);
            if (optEvent.isNone()) {
                logger.warn("Unable to find the event '{}'", eventId);
                return notFound();
            }

            Source eventSource = getIndexService().getEventSource(optEvent.get());
            if (eventSource == Source.ARCHIVE) {
                if (getAclService().applyAclToEpisode(eventId, accessControlList,
                        Option.<ConfiguredWorkflowRef>none())) {
                    return ok();
                } else {
                    logger.warn("Unable to find the event '{}'", eventId);
                    return notFound();
                }
            } else if (eventSource == Source.WORKFLOW) {
                logger.warn("An ACL cannot be edited while an event is part of a current workflow because it might"
                        + " lead to inconsistent ACLs i.e. changed after distribution so that the old ACL is still "
                        + "being used by the distribution channel.");
                return forbidden("Unable to edit an ACL for a current workflow.");
            } else {
                // The event doesn't exist as a mediapackage yet so use the scheduler service to update the ACL
                getSchedulerService().updateAccessControlList(getSchedulerService().getEventId(eventId),
                        accessControlList);
                return ok();
            }
        } catch (AclServiceException e) {
            logger.error("Error applying acl '{}' to event '{}' because: {}",
                    new Object[] { accessControlList, eventId, ExceptionUtils.getStackTrace(e) });
            return serverError();
        } catch (SchedulerException e) {
            logger.error("Error applying ACL to scheduled event {} because {}", eventId,
                    ExceptionUtils.getStackTrace(e));
            return serverError();
        } catch (SearchIndexException e) {
            logger.error("Error finding event {} to apply ACL because: {}", eventId,
                    ExceptionUtils.getStackTrace(e));
            return serverError();
        } catch (NotFoundException e) {
            throw e;
        }
    }

    @POST
    @Path("{eventId}/comment")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "createeventcomment", description = "Creates a comment related to the event given by the identifier", returnDescription = "The comment related to the event as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
                    @RestParameter(name = "text", isRequired = true, description = "The comment text", type = TEXT),
                    @RestParameter(name = "resolved", isRequired = false, description = "The comment resolved status", type = RestParameter.Type.BOOLEAN),
                    @RestParameter(name = "reason", isRequired = false, description = "The comment reason", type = STRING) }, reponses = {
                            @RestResponse(description = "The comment has been created.", responseCode = HttpServletResponse.SC_CREATED),
                            @RestResponse(description = "If no text ist set.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                            @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response createEventComment(@PathParam("eventId") String eventId, @FormParam("text") String text,
            @FormParam("reason") String reason, @FormParam("resolved") Boolean resolved) throws Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        if (StringUtils.isBlank(text))
            return Response.status(Status.BAD_REQUEST).build();

        User author = getSecurityService().getUser();
        try {
            Comment createdComment = Comment.create(Option.<Long>none(), text, author, reason,
                    BooleanUtils.toBoolean(reason));
            createdComment = getEventCommentService().updateComment(eventId, createdComment);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.created(getCommentUrl(eventId, createdComment.getId().get()))
                    .entity(createdComment.toJson().toJson()).build();
        } catch (Exception e) {
            logger.error("Unable to create a comment on the event {}: {}", eventId,
                    ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @POST
    @Path("{eventId}/comment/{commentId}")
    @RestQuery(name = "resolveeventcomment", description = "Resolves an event comment", returnDescription = "The resolved comment.", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, reponses = {
                    @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to resolve has not been found."),
                    @RestResponse(responseCode = SC_OK, description = "The resolved comment as JSON.") })
    public Response resolveEventComment(@PathParam("eventId") String eventId,
            @PathParam("commentId") long commentId) throws NotFoundException, Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            Comment dto = getEventCommentService().getComment(eventId, commentId);
            Comment updatedComment = Comment.create(dto.getId(), dto.getText(), dto.getAuthor(), dto.getReason(),
                    true, dto.getCreationDate(), new Date(), dto.getReplies());

            updatedComment = getEventCommentService().updateComment(eventId, updatedComment);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.ok(updatedComment.toJson().toJson()).build();
        } catch (NotFoundException e) {
            throw e;
        } catch (Exception e) {
            logger.error("Could not resolve comment {}: {}", commentId, ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @DELETE
    @Path("{eventId}/comment/{commentId}")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "deleteeventcomment", description = "Deletes a event related comment by its identifier", returnDescription = "No content", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", description = "The comment id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "The event related comment has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
                    @RestResponse(description = "No event or comment with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response deleteEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId)
            throws NotFoundException, Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            getEventCommentService().deleteComment(eventId, commentId);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.noContent().build();
        } catch (NotFoundException e) {
            throw e;
        } catch (Exception e) {
            logger.error("Unable to delete comment {} on event {}: {}",
                    new String[] { Long.toString(commentId), eventId, ExceptionUtils.getStackTrace(e) });
            throw new WebApplicationException(e);
        }
    }

    @DELETE
    @Path("{eventId}/comment/{commentId}/{replyId}")
    @RestQuery(name = "deleteeventreply", description = "Delete an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING),
            @RestParameter(name = "replyId", isRequired = true, description = "The comment reply identifier", type = STRING) }, reponses = {
                    @RestResponse(responseCode = SC_NOT_FOUND, description = "No event comment or reply with this identifier was found."),
                    @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
    public Response deleteEventCommentReply(@PathParam("eventId") String eventId,
            @PathParam("commentId") long commentId, @PathParam("replyId") long replyId)
            throws NotFoundException, Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        Comment comment = null;
        CommentReply reply = null;
        try {
            comment = getEventCommentService().getComment(eventId, commentId);
            for (CommentReply r : comment.getReplies()) {
                if (r.getId().isNone() || replyId != r.getId().get().longValue())
                    continue;
                reply = r;
                break;
            }

            if (reply == null)
                throw new NotFoundException("Reply with id " + replyId + " not found!");

            comment.removeReply(reply);

            Comment updatedComment = getEventCommentService().updateComment(eventId, comment);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.ok(updatedComment.toJson().toJson()).build();
        } catch (NotFoundException e) {
            throw e;
        } catch (Exception e) {
            logger.warn("Could not remove event comment reply {} from comment {}: {}", new String[] {
                    Long.toString(replyId), Long.toString(commentId), ExceptionUtils.getStackTrace(e) });
            throw new WebApplicationException(e);
        }
    }

    @PUT
    @Path("{eventId}/comment/{commentId}/{replyId}")
    @RestQuery(name = "updateeventcommentreply", description = "Updates an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING),
            @RestParameter(name = "replyId", isRequired = true, description = "The comment reply identifier", type = STRING) }, restParameters = {
                    @RestParameter(name = "text", isRequired = true, description = "The comment reply text", type = TEXT) }, reponses = {
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to extend with a reply or the reply has not been found."),
                            @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "If no text is set."),
                            @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
    public Response updateEventCommentReply(@PathParam("eventId") String eventId,
            @PathParam("commentId") long commentId, @PathParam("replyId") long replyId,
            @FormParam("text") String text) throws NotFoundException, Exception {
        if (StringUtils.isBlank(text))
            return Response.status(Status.BAD_REQUEST).build();

        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        Comment comment = null;
        CommentReply reply = null;
        try {
            comment = getEventCommentService().getComment(eventId, commentId);
            for (CommentReply r : comment.getReplies()) {
                if (r.getId().isNone() || replyId != r.getId().get().longValue())
                    continue;
                reply = r;
                break;
            }

            if (reply == null)
                throw new NotFoundException("Reply with id " + replyId + " not found!");

            CommentReply updatedReply = CommentReply.create(reply.getId(), text.trim(), reply.getAuthor(),
                    reply.getCreationDate(), new Date());
            comment.removeReply(reply);
            comment.addReply(updatedReply);

            Comment updatedComment = getEventCommentService().updateComment(eventId, comment);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.ok(updatedComment.toJson().toJson()).build();
        } catch (NotFoundException e) {
            throw e;
        } catch (Exception e) {
            logger.warn("Could not update event comment reply {} from comment {}: {}", new String[] {
                    Long.toString(replyId), Long.toString(commentId), ExceptionUtils.getStackTrace(e) });
            throw new WebApplicationException(e);
        }
    }

    @POST
    @Path("{eventId}/comment/{commentId}/reply")
    @RestQuery(name = "createeventcommentreply", description = "Creates an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, restParameters = {
                    @RestParameter(name = "text", isRequired = true, description = "The comment reply text", type = TEXT),
                    @RestParameter(name = "resolved", isRequired = false, description = "Flag defining if this reply solve or not the comment.", type = BOOLEAN) }, reponses = {
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to extend with a reply has not been found."),
                            @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "If no text is set."),
                            @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
    public Response createEventCommentReply(@PathParam("eventId") String eventId,
            @PathParam("commentId") long commentId, @FormParam("text") String text,
            @FormParam("resolved") Boolean resolved) throws NotFoundException, Exception {
        if (StringUtils.isBlank(text))
            return Response.status(Status.BAD_REQUEST).build();

        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        Comment comment = null;
        try {
            comment = getEventCommentService().getComment(eventId, commentId);
            Comment updatedComment;

            if (resolved != null && resolved) {
                // If the resolve flag is set to true, change to comment to resolved
                updatedComment = Comment.create(comment.getId(), comment.getText(), comment.getAuthor(),
                        comment.getReason(), true, comment.getCreationDate(), new Date(), comment.getReplies());
            } else {
                updatedComment = comment;
            }

            User author = getSecurityService().getUser();
            CommentReply reply = CommentReply.create(Option.<Long>none(), text, author);
            updatedComment.addReply(reply);

            updatedComment = getEventCommentService().updateComment(eventId, updatedComment);
            List<Comment> comments = getEventCommentService().getComments(eventId);
            updateCommentCatalog(optEvent.get(), comments);
            return Response.ok(updatedComment.toJson().toJson()).build();
        } catch (Exception e) {
            logger.warn("Could not create event comment reply on comment {}: {}", comment,
                    ExceptionUtils.getStackTrace(e));
            throw new WebApplicationException(e);
        }
    }

    @GET
    @Path("{eventId}/metadata.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventmetadata", description = "Returns all the data related to the metadata tab in the event details modal as JSON", returnDescription = "All the data related to the event metadata tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event metadata tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventMetadata(@PathParam("eventId") String eventId) throws Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        MetadataList metadataList = new MetadataList();
        List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
        catalogUIAdapters.remove(getEpisodeCatalogUIAdapter());
        Opt<MediaPackage> optMediaPackage = getIndexService().getEventMediapackage(optEvent.get());
        if (catalogUIAdapters.size() > 0) {
            if (optMediaPackage.isSome()) {
                for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
                    metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(optMediaPackage.get()));
                }
            }
        }
        metadataList.add(getEpisodeCatalogUIAdapter(), getEventMetadata(optEvent.get()));

        if (WorkflowInstance.WorkflowState.RUNNING.toString().equals(optEvent.get().getWorkflowState()))
            metadataList.setLocked(Locked.WORKFLOW_RUNNING);

        return okJson(metadataList.toJSON());
    }

    /**
     * Loads the metadata for the given event
     *
     * @param event
     *          the source {@link Event}
     * @return a {@link AbstractMetadataCollection} instance with all the event metadata
     */
    @SuppressWarnings("unchecked")
    private AbstractMetadataCollection getEventMetadata(Event event) throws Exception {
        AbstractMetadataCollection metadata = getEpisodeCatalogUIAdapter().getRawFields();

        MetadataField<?> title = metadata.getOutputFields().get("title");
        metadata.removeField(title);
        MetadataField<String> newTitle = MetadataUtils.copyMetadataField(title);
        newTitle.setValue(event.getTitle());
        metadata.addField(newTitle);

        MetadataField<?> subject = metadata.getOutputFields().get("subject");
        metadata.removeField(subject);
        MetadataField<String> newSubject = MetadataUtils.copyMetadataField(subject);
        newSubject.setValue(event.getSubject());
        metadata.addField(newSubject);

        MetadataField<?> description = metadata.getOutputFields().get("description");
        metadata.removeField(description);
        MetadataField<String> newDescription = MetadataUtils.copyMetadataField(description);
        newDescription.setValue(event.getDescription());
        metadata.addField(newDescription);

        MetadataField<?> language = metadata.getOutputFields().get("language");
        metadata.removeField(language);
        MetadataField<String> newLanguage = MetadataUtils.copyMetadataField(language);
        newLanguage.setValue(event.getLanguage());
        metadata.addField(newLanguage);

        MetadataField<?> rightsHolder = metadata.getOutputFields().get("rightsHolder");
        metadata.removeField(rightsHolder);
        MetadataField<String> newRightsHolder = MetadataUtils.copyMetadataField(rightsHolder);
        newRightsHolder.setValue(event.getRights());
        metadata.addField(newRightsHolder);

        MetadataField<?> license = metadata.getOutputFields().get("license");
        metadata.removeField(license);
        MetadataField<String> newLicense = MetadataUtils.copyMetadataField(license);
        newLicense.setValue(event.getLicense());
        metadata.addField(newLicense);

        MetadataField<?> series = metadata.getOutputFields().get("isPartOf");
        metadata.removeField(series);
        MetadataField<String> newSeries = MetadataUtils.copyMetadataField(series);
        newSeries.setValue(event.getSeriesId());
        metadata.addField(newSeries);

        MetadataField<?> presenters = metadata.getOutputFields().get("creator");
        metadata.removeField(presenters);
        MetadataField<String> newPresenters = MetadataUtils.copyMetadataField(presenters);
        newPresenters.setValue(StringUtils.join(event.getPresenters(), ", "));
        metadata.addField(newPresenters);

        MetadataField<?> contributors = metadata.getOutputFields().get("contributor");
        metadata.removeField(contributors);
        MetadataField<String> newContributors = MetadataUtils.copyMetadataField(contributors);
        newContributors.setValue(StringUtils.join(event.getContributors(), ", "));
        metadata.addField(newContributors);

        String recordingStartDate = event.getRecordingStartDate();
        if (StringUtils.isNotBlank(recordingStartDate)) {
            Date startDateTime = new Date(DateTimeSupport.fromUTC(recordingStartDate));

            MetadataField<?> startDate = metadata.getOutputFields().get("startDate");
            metadata.removeField(startDate);
            MetadataField<String> newStartDate = MetadataUtils.copyMetadataField(startDate);
            SimpleDateFormat sdf = new SimpleDateFormat(startDate.getPattern().get());
            newStartDate.setValue(sdf.format(startDateTime));
            metadata.addField(newStartDate);

            MetadataField<?> startTime = metadata.getOutputFields().get("startTime");
            metadata.removeField(startTime);
            MetadataField<String> newStartTime = MetadataUtils.copyMetadataField(startTime);
            sdf = new SimpleDateFormat(startTime.getPattern().get());
            newStartTime.setValue(sdf.format(startDateTime));
            metadata.addField(newStartTime);
        }

        if (event.getDuration() != null) {
            MetadataField<?> duration = metadata.getOutputFields().get("duration");
            metadata.removeField(duration);
            MetadataField<String> newDuration = MetadataUtils.copyMetadataField(duration);
            newDuration.setValue(event.getDuration().toString());
            metadata.addField(newDuration);
        }

        MetadataField<?> agent = metadata.getOutputFields().get("agent");
        metadata.removeField(agent);
        MetadataField<String> newAgent = MetadataUtils.copyMetadataField(agent);
        newAgent.setValue(event.getLocation());
        metadata.addField(newAgent);

        MetadataField<?> source = metadata.getOutputFields().get("source");
        metadata.removeField(source);
        MetadataField<String> newSource = MetadataUtils.copyMetadataField(source);
        newSource.setValue(event.getSource());
        metadata.addField(newSource);

        // Admin UI only field
        MetadataField<String> createdBy = MetadataField.createTextMetadataField("createdBy", Opt.<String>none(),
                "EVENTS.EVENTS.DETAILS.METADATA.CREATED_BY", true, false, Opt.<Map<String, Object>>none(),
                Opt.<String>none(), Opt.some(CREATED_BY_UI_ORDER), Opt.<String>none());
        createdBy.setValue(event.getCreator());
        metadata.addField(createdBy);

        MetadataField<?> created = metadata.getOutputFields().get("created");
        metadata.removeField(created);
        MetadataField<Date> newCreated = MetadataUtils.copyMetadataField(created);
        newCreated.setValue(new Date(DateTimeSupport.fromUTC(event.getCreated())));
        metadata.addField(newCreated);

        MetadataField<?> uid = metadata.getOutputFields().get("uid");
        metadata.removeField(uid);
        MetadataField<String> newUID = MetadataUtils.copyMetadataField(uid);
        newUID.setValue(event.getIdentifier());
        metadata.addField(newUID);

        return metadata;
    }

    private List<EventCatalogUIAdapter> getEventCatalogUIAdapters() {
        return new ArrayList<EventCatalogUIAdapter>(
                getEventCatalogUIAdapters(getSecurityService().getOrganization().getId()));
    }

    private MetadataList getMetadatListWithAllEventCatalogUIAdapters() {
        MetadataList metadataList = new MetadataList();
        for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
            metadataList.add(catalogUIAdapter, catalogUIAdapter.getRawFields());
        }
        return metadataList;
    }

    @PUT
    @Path("{eventId}/metadata")
    @RestQuery(name = "updateeventmetadata", description = "Update the passed metadata for the event with the given Id", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
                    @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of metadata to update") }, reponses = {
                            @RestResponse(description = "The metadata have been updated.", responseCode = HttpServletResponse.SC_OK),
                            @RestResponse(description = "Could not parse metadata.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                            @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "No content is returned.")
    public Response updateEventMetadata(@PathParam("eventId") String id, @FormParam("metadata") String metadataJSON)
            throws UnauthorizedException, Exception {
        MetadataList metadataList = getIndexService().updateAllEventMetadata(id, metadataJSON, getIndex());
        return okJson(metadataList.toJSON());
    }

    private void updateMediaPackageCommentCatalog(MediaPackage mediaPackage, List<Comment> comments)
            throws CommentException, IOException {
        // Get the comments catalog
        Catalog[] commentCatalogs = mediaPackage.getCatalogs(MediaPackageElements.COMMENTS);
        Catalog c = null;
        if (commentCatalogs.length == 1)
            c = commentCatalogs[0];

        if (comments.size() > 0) {
            // If no comments catalog found, create a new one
            if (c == null) {
                c = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
                        .newElement(Type.Catalog, MediaPackageElements.COMMENTS);
                c.setIdentifier(UUID.randomUUID().toString());
                mediaPackage.add(c);
            }

            // Update comments catalog
            InputStream in = null;
            try {
                String commentCatalog = CommentParser.getAsXml(comments);
                in = IOUtils.toInputStream(commentCatalog, "UTF-8");
                URI uri = getWorkspace().put(mediaPackage.getIdentifier().toString(), c.getIdentifier(),
                        "comments.xml", in);
                c.setURI(uri);
                // setting the URI to a new source so the checksum will most like be invalid
                c.setChecksum(null);
            } finally {
                IOUtils.closeQuietly(in);
            }
        } else {
            // Remove comments catalog
            if (c != null) {
                mediaPackage.remove(c);
                try {
                    getWorkspace().delete(c.getURI());
                } catch (NotFoundException e) {
                    logger.warn("Comments catalog {} not found to delete!", c.getURI());
                }
            }
        }
    }

    @GET
    @Path("{eventId}/media.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventmedia", description = "Returns all the data related to the media tab in the event details modal as JSON", returnDescription = "All the data related to the event media tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event media tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventMedia(@PathParam("eventId") String id) throws Exception {
        Opt<Event> optEvent = getEvent(id);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", id);

        Opt<MediaPackage> mpOpt = getIndexService().getEventMediapackage(optEvent.get());

        List<JValue> tracksJSON = new ArrayList<JValue>();
        if (mpOpt.isSome()) {
            for (Track track : mpOpt.get().getTracks()) {
                tracksJSON.add(j(f("id", vN(track.getIdentifier())), f("type", vN(track.getFlavor().toString())),
                        f("mimetype", vN(track.getMimeType())), f("url", vN(track.getURI()))));
            }
        }

        return okJson(a(tracksJSON));
    }

    @GET
    @Path("{eventId}/media/{trackId}.json")
    @RestQuery(name = "geteventtrack", description = "Returns all the data related to the media/track tab in the event details modal as JSON", returnDescription = "All the data related to the given track for the media tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "trackId", description = "The track id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the given track for the media tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventTrack(@PathParam("eventId") String eventId, @PathParam("trackId") String trackId)
            throws Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        JValue result = Jsons.jz;
        Opt<MediaPackage> mpOpt = getIndexService().getEventMediapackage(optEvent.get());
        if (mpOpt.isSome()) {

            Track track = mpOpt.get().getTrack(trackId);
            if (track == null)
                return notFound("Cannot find a track with id '%s' on event with id '%s'.", trackId, eventId);

            org.opencastproject.mediapackage.Stream[] streams = track.getStreams();
            List<JValue> audioStreamsJSON = new ArrayList<JValue>();
            List<JValue> videoStreamsJSON = new ArrayList<JValue>();
            for (org.opencastproject.mediapackage.Stream stream : streams) {

                if (stream instanceof AudioStreamImpl) {
                    AudioStream audioStream = (AudioStream) stream;
                    // TODO There is a bug with the stream ids, see MH-10325, so ignoring for now
                    JField id = f("id", vN(audioStream.getIdentifier()));

                    audioStreamsJSON.add(
                            j(f("type", vN(audioStream.getFormat())), f("channels", vN(audioStream.getChannels())),
                                    f("bitrate", vN(audioStream.getBitRate()))));
                } else if (stream instanceof VideoStreamImpl) {
                    VideoStream videoStream = (VideoStream) stream;
                    // TODO There is a bug with the stream ids, see MH-10325, so ignoring for now
                    JField id = f("id", vN(videoStream.getIdentifier()));

                    videoStreamsJSON.add(j(f("type", vN(videoStream.getFormat())),
                            f("bitrate", v(videoStream.getBitRate())),
                            f("framerate", vN(videoStream.getFrameRate())),
                            f("resolution", vN(videoStream.getFrameWidth() + "x" + videoStream.getFrameHeight()))));
                } else {
                    throw new IllegalArgumentException("stream must be either audio or video");
                }
            }
            result = j(f("id", vN(track.getIdentifier())), f("type", vN(track.getElementType())),
                    f("duration", vN(track.getDuration())), f("mimetype", vN(track.getMimeType())),
                    f("flavor", vN(track.getFlavor())), f("url", vN(track.getURI())),
                    f("description", vN(track.getDescription())),
                    f("tags", vN(StringUtils.join(track.getTags(), ","))),
                    f("streams", j(f("audio", a(audioStreamsJSON)), f("video", a(videoStreamsJSON)))));
        }
        return okJson(result);
    }

    @GET
    @Path("{eventId}/attachments.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventattachements", description = "Returns all the data related to the attachements tab in the event details modal as JSON", returnDescription = "All the data related to the event attachements tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event attachements tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventAttachements(@PathParam("eventId") String id) throws Exception {
        Opt<Event> optEvent = getEvent(id);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", id);

        Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get());

        List<JValue> attachementsJSON = new ArrayList<JValue>();
        if (mp.isSome()) {
            for (Attachment attachement : mp.get().getAttachments()) {
                attachement.getMediaPackage();
                attachementsJSON.add(j(f("id", vN(attachement.getIdentifier())),
                        f("type", vN(attachement.getFlavor().toString())),
                        f("mimetype", vN(attachement.getMimeType())),
                        f("tags", vN(StringUtils.join(attachement.getTags(), ","))),
                        f("url", vN(attachement.getURI()))));
            }
        }

        return okJson(a(attachementsJSON));
    }

    @GET
    @Path("{eventId}/workflows.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventworkflows", description = "Returns all the data related to the workflows tab in the event details modal as JSON", returnDescription = "All the data related to the event workflows tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event workflows tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventWorkflows(@PathParam("eventId") String id)
            throws WorkflowDatabaseException, JobEndpointException, SearchIndexException {
        Opt<Event> optEvent = getEvent(id);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", id);

        WorkflowQuery query = new WorkflowQuery().withMediaPackage(id);

        try {
            return okJson(getJobService().getTasksAsJSON(query));
        } catch (NotFoundException e) {
            return notFound("Cannot find workflows for event %s", id);
        }
    }

    @GET
    @Path("{eventId}/workflows/{workflowId}")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventworkflow", description = "Returns all the data related to the single workflow tab in the event details modal as JSON", returnDescription = "All the data related to the event singe workflow tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event single workflow tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventWorkflow(@PathParam("eventId") String eventId,
            @PathParam("workflowId") String workflowId)
            throws WorkflowDatabaseException, JobEndpointException, SearchIndexException {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        long workflowInstanceId;
        try {
            workflowId = StringUtils.remove(workflowId, ".json");
            workflowInstanceId = Long.parseLong(workflowId);
        } catch (Exception e) {
            logger.warn("Unable to parse workflow id {}", workflowId);
            return RestUtil.R.badRequest();
        }

        try {
            return okJson(getJobService().getTasksAsJSON(workflowInstanceId));
        } catch (NotFoundException e) {
            return notFound("Cannot find workflow  %s", workflowId);
        }
    }

    @GET
    @Path("{eventId}/workflows/{workflowId}/operations.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventoperations", description = "Returns all the data related to the workflow/operations tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/opertations tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event workflow/operations tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventOperations(@PathParam("eventId") String eventId,
            @PathParam("workflowId") String workflowId)
            throws WorkflowDatabaseException, JobEndpointException, SearchIndexException {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        long workflowInstanceId;
        try {
            workflowInstanceId = Long.parseLong(workflowId);
        } catch (Exception e) {
            logger.warn("Unable to parse workflow id {}", workflowId);
            return RestUtil.R.badRequest();
        }

        try {
            return okJson(getJobService().getOperationsAsJSON(workflowInstanceId));
        } catch (NotFoundException e) {
            return notFound("Cannot find workflow %s", workflowId);
        }
    }

    @GET
    @Path("{eventId}/workflows/{workflowId}/operations/{operationPosition}")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventoperation", description = "Returns all the data related to the workflow/operation tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/opertation tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "operationPosition", description = "The operation position", isRequired = true, type = RestParameter.Type.INTEGER) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event workflow/operation tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "Unable to parse workflowId or operationPosition", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                    @RestResponse(description = "No operation with these identifiers was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventOperation(@PathParam("eventId") String eventId,
            @PathParam("workflowId") String workflowId, @PathParam("operationPosition") Integer operationPosition)
            throws WorkflowDatabaseException, JobEndpointException, SearchIndexException {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        long workflowInstanceId;
        try {
            workflowInstanceId = Long.parseLong(workflowId);
        } catch (Exception e) {
            logger.warn("Unable to parse workflow id {}", workflowId);
            return RestUtil.R.badRequest();
        }

        try {
            return okJson(getJobService().getOperationAsJSON(workflowInstanceId, operationPosition));
        } catch (NotFoundException e) {
            return notFound("Cannot find workflow %s", workflowId);
        }
    }

    @GET
    @Path("{eventId}/workflows/{workflowId}/errors.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventerrors", description = "Returns all the data related to the workflow/errors tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/errors tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event workflow/errors tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventErrors(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
            @Context HttpServletRequest req)
            throws WorkflowDatabaseException, JobEndpointException, SearchIndexException {
        // the call to #getEvent should make sure that the calling user has access rights to the workflow
        // FIXME since there is no dependency between the event and the workflow (the fetched event is
        // simply ignored) an attacker can get access by using an event he owns and a workflow ID of
        // someone else.
        for (final Event ignore : getEvent(eventId)) {
            final long workflowIdLong;
            try {
                workflowIdLong = Long.parseLong(workflowId);
            } catch (Exception e) {
                logger.warn("Unable to parse workflow id {}", workflowId);
                return RestUtil.R.badRequest();
            }
            try {
                return okJson(getJobService().getIncidentsAsJSON(workflowIdLong, req.getLocale(), true));
            } catch (NotFoundException e) {
                return notFound("Cannot find the incident for the workflow %s", workflowId);
            }
        }
        return notFound("Cannot find an event with id '%s'.", eventId);
    }

    @GET
    @Path("{eventId}/workflows/{workflowId}/errors/{errorId}.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "geteventerror", description = "Returns all the data related to the workflow/error tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/error tab as JSON", pathParameters = {
            @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "errorId", description = "The error id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(description = "Returns all the data related to the event workflow/error tab as JSON", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
                    @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
    public Response getEventError(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
            @PathParam("errorId") String errorId, @Context HttpServletRequest req)
            throws WorkflowDatabaseException, JobEndpointException, SearchIndexException {
        // the call to #getEvent should make sure that the calling user has access rights to the workflow
        // FIXME since there is no dependency between the event and the workflow (the fetched event is
        // simply ignored) an attacker can get access by using an event he owns and a workflow ID of
        // someone else.
        for (Event ignore : getEvent(eventId)) {
            final long errorIdLong;
            try {
                errorIdLong = Long.parseLong(errorId);
            } catch (Exception e) {
                logger.warn("Unable to parse error id {}", errorId);
                return RestUtil.R.badRequest();
            }
            try {
                return okJson(getJobService().getIncidentAsJSON(errorIdLong, req.getLocale()));
            } catch (NotFoundException e) {
                return notFound("Cannot find the incident %s", errorId);
            }
        }
        return notFound("Cannot find an event with id '%s'.", eventId);
    }

    @GET
    @Path("{eventId}/access.json")
    @SuppressWarnings("unchecked")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "getEventAccessInformation", description = "Get the access information of an event", returnDescription = "The access information", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the request."),
                    @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."),
                    @RestResponse(responseCode = SC_OK, description = "The access information ") })
    public Response getEventAccessInformation(@PathParam("eventId") String eventId) throws Exception {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        // Add all available ACLs to the response
        JSONArray systemAclsJson = new JSONArray();
        List<ManagedAcl> acls = getAclService().getAcls();
        for (ManagedAcl acl : acls) {
            systemAclsJson.add(AccessInformationUtil.serializeManagedAcl(acl));
        }

        // Get the episode ACL
        final TransitionQuery q = TransitionQuery.query().withId(eventId).withScope(AclScope.Episode);
        List<EpisodeACLTransition> episodeTransistions;
        JSONArray transitionsJson = new JSONArray();
        try {
            episodeTransistions = getAclService().getTransitions(q).getEpisodeTransistions();
            for (EpisodeACLTransition trans : episodeTransistions) {
                transitionsJson.add(AccessInformationUtil.serializeEpisodeACLTransition(trans));
            }
        } catch (AclServiceException e) {
            logger.error(
                    "There was an error while trying to get the ACL transitions for series '{}' from the ACL service: {}",
                    eventId, ExceptionUtils.getStackTrace(e));
            return RestUtil.R.serverError();
        }

        AccessControlList activeAcl = new AccessControlList();
        try {
            activeAcl = AccessControlParser.parseAcl(optEvent.get().getAccessPolicy());
        } catch (Exception e) {
            logger.error("Unable to parse access policy because: {}", ExceptionUtils.getStackTrace(e));
        }
        Option<ManagedAcl> currentAcl = AccessInformationUtil.matchAcls(acls, activeAcl);

        JSONObject episodeAccessJson = new JSONObject();
        episodeAccessJson.put("current_acl", currentAcl.isSome() ? currentAcl.get().getId() : 0L);
        episodeAccessJson.put("acl", AccessControlParser.toJsonSilent(activeAcl));
        episodeAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(activeAcl));
        episodeAccessJson.put("transitions", transitionsJson);
        if (StringUtils.isNotBlank(optEvent.get().getWorkflowState())
                && WorkflowUtil.isActive(WorkflowState.valueOf(optEvent.get().getWorkflowState())))
            episodeAccessJson.put("locked", true);

        JSONObject jsonReturnObj = new JSONObject();
        jsonReturnObj.put("episode_access", episodeAccessJson);
        jsonReturnObj.put("system_acls", systemAclsJson);

        return Response.ok(jsonReturnObj.toString()).build();
    }

    @POST
    @Path("{eventId}/transitions")
    @RestQuery(name = "addEventTransition", description = "Adds an ACL transition to an event", returnDescription = "The method doesn't return any content", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, restParameters = {
                    @RestParameter(name = "transition", isRequired = true, description = "The transition (JSON object) to add", type = RestParameter.Type.TEXT) }, reponses = {
                            @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required params were missing in the request."),
                            @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content"),
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found.") })
    public Response addEventTransition(@PathParam("eventId") String eventId,
            @FormParam("transition") String transitionStr) throws SearchIndexException {
        if (StringUtils.isBlank(eventId) || StringUtils.isBlank(transitionStr))
            return RestUtil.R.badRequest("Missing parameters");

        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            final org.codehaus.jettison.json.JSONObject t = new org.codehaus.jettison.json.JSONObject(
                    transitionStr);
            Option<ConfiguredWorkflowRef> workflowRef;
            if (t.has("workflow_id"))
                workflowRef = Option.some(ConfiguredWorkflowRef.workflow(t.getString("workflow_id")));
            else
                workflowRef = Option.none();

            Option<Long> managedAclId;
            if (t.has("acl_id"))
                managedAclId = Option.some(t.getLong("acl_id"));
            else
                managedAclId = Option.none();

            getAclService().addEpisodeTransition(eventId, managedAclId,
                    new Date(DateTimeSupport.fromUTC(t.getString("application_date"))), workflowRef);
            return Response.noContent().build();
        } catch (AclServiceException e) {
            logger.error("Error while trying to get ACL transitions for event '{}' from ACL service: {}", eventId,
                    e);
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        } catch (JSONException e) {
            return RestUtil.R.badRequest("The transition object is not valid");
        } catch (IllegalStateException e) {
            // That should never happen
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        } catch (ParseException e) {
            return RestUtil.R.badRequest("The date could not be parsed");
        }
    }

    @PUT
    @Path("{eventId}/transitions/{transitionId}")
    @RestQuery(name = "updateEventTransition", description = "Updates an ACL transition of an event", returnDescription = "The method doesn't return any content", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING),
            @RestParameter(name = "transitionId", isRequired = true, description = "The transition identifier", type = RestParameter.Type.INTEGER) }, restParameters = {
                    @RestParameter(name = "transition", isRequired = true, description = "The updated transition (JSON object)", type = RestParameter.Type.TEXT) }, reponses = {
                            @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required params were missing in the request."),
                            @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or transtion has not been found."),
                            @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content") })
    public Response updateEventTransition(@PathParam("eventId") String eventId,
            @PathParam("transitionId") long transitionId, @FormParam("transition") String transitionStr)
            throws NotFoundException, SearchIndexException {
        if (StringUtils.isBlank(transitionStr))
            return RestUtil.R.badRequest("Missing parameters");

        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            final org.codehaus.jettison.json.JSONObject t = new org.codehaus.jettison.json.JSONObject(
                    transitionStr);
            Option<ConfiguredWorkflowRef> workflowRef;
            if (t.has("workflow_id"))
                workflowRef = Option.some(ConfiguredWorkflowRef.workflow(t.getString("workflow_id")));
            else
                workflowRef = Option.none();

            Option<Long> managedAclId;
            if (t.has("acl_id"))
                managedAclId = Option.some(t.getLong("acl_id"));
            else
                managedAclId = Option.none();

            getAclService().updateEpisodeTransition(transitionId, managedAclId,
                    new Date(DateTimeSupport.fromUTC(t.getString("application_date"))), workflowRef);
            return Response.noContent().build();
        } catch (JSONException e) {
            return RestUtil.R.badRequest("The transition object is not valid");
        } catch (IllegalStateException e) {
            // That should never happen
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        } catch (AclServiceException e) {
            logger.error("Unable to update transtion {} of event {}: {}",
                    new String[] { Long.toString(transitionId), eventId, ExceptionUtils.getStackTrace(e) });
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        } catch (ParseException e) {
            // That should never happen
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        }
    }

    @PUT
    @Path("{eventId}/optout/{optout}")
    @RestQuery(name = "updateEventOptoutStatus", description = "Updates an event's opt out status.", returnDescription = "The method doesn't return any content", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING),
            @RestParameter(name = "optout", isRequired = true, description = "True or false, true to opt out of this recording.", type = RestParameter.Type.BOOLEAN) }, restParameters = {}, reponses = {
                    @RestResponse(responseCode = SC_NOT_FOUND, description = "The event has not been found"),
                    @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content") })
    public Response updateEventOptOut(@PathParam("eventId") String eventId, @PathParam("optout") boolean optout)
            throws NotFoundException {
        try {
            return changeOptOutStatus(eventId, optout);
        } catch (NotFoundException e) {
            throw e;
        } catch (SchedulerException e) {
            logger.error("Unable to updated opt out status for event with id {}", eventId);
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        } catch (SearchIndexException e) {
            logger.error("Unable to get event with id {}", eventId);
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        }

    }

    @POST
    @Path("optouts")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "changeOptOuts", description = "Change the opt out status of many events", returnDescription = "A JSON array listing which events were or were not opted out.", restParameters = {
            @RestParameter(name = "eventIds", description = "A JSON array of ids of the events to opt out or in", defaultValue = "[]", isRequired = true, type = RestParameter.Type.STRING),
            @RestParameter(name = "optout", description = "Whether to opt out or not either true or false.", defaultValue = "false", isRequired = true, type = RestParameter.Type.BOOLEAN), }, reponses = {
                    @RestResponse(description = "Returns a JSON object with the results for the different opted out or in elements such as ok, notFound or error.", responseCode = HttpServletResponse.SC_OK),
                    @RestResponse(description = "Unable to parse boolean value to opt out, or parse JSON array of opt out events", responseCode = HttpServletResponse.SC_BAD_REQUEST) })
    public Response changeOptOuts(@FormParam("optout") boolean optout, @FormParam("eventIds") String eventIds) {
        JSONArray eventIdsArray;
        try {
            eventIdsArray = (JSONArray) parser.parse(eventIds);
        } catch (org.json.simple.parser.ParseException e) {
            logger.warn("Unable to parse event ids {} : {}", eventIds, ExceptionUtils.getStackTrace(e));
            return Response.status(Status.BAD_REQUEST).build();
        } catch (NullPointerException e) {
            logger.warn("Unable to parse event ids because it was null {}", eventIds);
            return Response.status(Status.BAD_REQUEST).build();
        } catch (ClassCastException e) {
            logger.warn("Unable to parse event ids because it was the wrong class {} : {}", eventIds,
                    ExceptionUtils.getStackTrace(e));
            return Response.status(Status.BAD_REQUEST).build();
        }

        BulkOperationResult result = new BulkOperationResult();

        for (Object idObject : eventIdsArray) {
            String eventId = idObject.toString();
            try {
                Response response = changeOptOutStatus(eventId, optout);
                if (response.getStatus() == HttpStatus.SC_NO_CONTENT) {
                    result.addOk(eventId);
                } else if (response.getStatus() == HttpStatus.SC_NOT_FOUND) {
                    result.addNotFound(eventId);
                }
            } catch (NotFoundException e) {
                result.addNotFound(idObject.toString());
            } catch (Exception e) {
                logger.error("Could not update opt out status of event {}: {}", eventId,
                        ExceptionUtils.getStackTrace(e));
                result.addServerError(idObject.toString());
            }

        }
        return Response.ok(result.toJson()).build();
    }

    /**
     * Changes the opt out status of a single event (by its mediapackage id)
     *
     * @param eventId
     *          The event's unique id formally the mediapackage id
     * @param optout
     *          Whether the event should be moved into optted out.
     * @return A HTTP Response of no content if everything is okay, not found or server error if not.
     */
    private Response changeOptOutStatus(String eventId, boolean optout)
            throws NotFoundException, SchedulerException, SearchIndexException {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        getSchedulerService().updateOptOutStatus(eventId, optout);
        logger.debug("Setting event {} to opt out status of {}", eventId, optout);
        return Response.noContent().build();
    }

    @DELETE
    @Path("{eventId}/transitions/{transitionId}")
    @RestQuery(name = "deleteEventTransition", description = "Deletes an ACL transition from an event", returnDescription = "The method doesn't return any content", pathParameters = {
            @RestParameter(name = "eventId", isRequired = true, description = "The series identifier", type = RestParameter.Type.STRING),
            @RestParameter(name = "transitionId", isRequired = true, description = "The transition identifier", type = RestParameter.Type.INTEGER) }, reponses = {
                    @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or the transition has not been found."),
                    @RestResponse(responseCode = SC_NO_CONTENT, description = "The method does not return any content") })
    public Response deleteEventTransition(@PathParam("eventId") String eventId,
            @PathParam("transitionId") long transitionId) throws NotFoundException, SearchIndexException {
        Opt<Event> optEvent = getEvent(eventId);
        if (optEvent.isNone())
            return notFound("Cannot find an event with id '%s'.", eventId);

        try {
            getAclService().deleteEpisodeTransition(transitionId);
            return Response.noContent().build();
        } catch (AclServiceException e) {
            logger.error("Error while trying to delete transition '{}' from event '{}': {}",
                    new String[] { Long.toString(transitionId), eventId, ExceptionUtils.getStackTrace(e) });
            throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
        }
    }

    @GET
    @Path("new/metadata")
    @RestQuery(name = "getNewMetadata", description = "Returns all the data related to the metadata tab in the new event modal as JSON", returnDescription = "All the data related to the event metadata tab as JSON", reponses = {
            @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event metadata tab as JSON") })
    public Response getNewMetadata() {
        MetadataList metadataList = getMetadatListWithAllEventCatalogUIAdapters();
        Opt<AbstractMetadataCollection> optMetadataByAdapter = metadataList
                .getMetadataByAdapter(getEpisodeCatalogUIAdapter());
        if (optMetadataByAdapter.isSome()) {
            AbstractMetadataCollection collection = optMetadataByAdapter.get();
            collection.removeField(collection.getOutputFields().get("created"));
            collection.removeField(collection.getOutputFields().get("duration"));
            collection.removeField(collection.getOutputFields().get("uid"));
            collection.removeField(collection.getOutputFields().get("source"));
            collection.removeField(collection.getOutputFields().get("startDate"));
            collection.removeField(collection.getOutputFields().get("startTime"));
            collection.removeField(collection.getOutputFields().get("agent"));
            metadataList.add(getEpisodeCatalogUIAdapter(), collection);
        }
        return okJson(metadataList.toJSON());
    }

    @GET
    @Path("new/processing")
    @RestQuery(name = "getNewProcessing", description = "Returns all the data related to the processing tab in the new event modal as JSON", returnDescription = "All the data related to the event processing tab as JSON", restParameters = {
            @RestParameter(name = "tags", isRequired = false, description = "A comma separated list of tags to filter the workflow definitions", type = RestParameter.Type.STRING) }, reponses = {
                    @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event processing tab as JSON") })
    public Response getNewProcessing(@QueryParam("tags") String tagsString) {
        List<String> tags = RestUtil.splitCommaSeparatedParam(Option.option(tagsString)).value();

        // This is the JSON Object which will be returned by this request
        List<JValue> actions = new ArrayList<JValue>();
        try {
            List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions();
            for (WorkflowDefinition wflDef : workflowsDefinitions) {
                if (wflDef.containsTag(tags)) {

                    actions.add(j(f("id", v(wflDef.getId())), f("title", v(Opt.nul(wflDef.getTitle()).or(""))),
                            f("description", v(Opt.nul(wflDef.getDescription()).or(""))),
                            f("configuration_panel", v(Opt.nul(wflDef.getConfigurationPanel()).or("")))));
                }
            }
        } catch (WorkflowDatabaseException e) {
            logger.error("Unable to get available workflow definitions: {}", ExceptionUtils.getStackTrace(e));
            return RestUtil.R.serverError();
        }

        return okJson(a(actions));
    }

    @POST
    @Path("new/conflicts")
    @RestQuery(name = "checkNewConflicts", description = "Checks if the current scheduler parameters are in a conflict with another event", returnDescription = "Returns NO CONTENT if no event are in conflict within specified period or list of conflicting recordings in JSON", restParameters = {
            @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, reponses = {
                    @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
                    @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"),
                    @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters") })
    public Response getNewConflicts(@FormParam("metadata") String metadata) throws NotFoundException {
        if (StringUtils.isBlank(metadata)) {
            logger.warn("Metadata is not specified");
            return Response.status(Status.BAD_REQUEST).build();
        }

        JSONObject metadataJson;
        try {
            metadataJson = (JSONObject) parser.parse(metadata);
        } catch (Exception e) {
            logger.warn("Unable to parse metadata {}", metadata);
            return RestUtil.R.badRequest("Unable to parse metadata");
        }

        String device;
        String startDate;
        String endDate;
        try {
            device = (String) metadataJson.get("device");
            startDate = (String) metadataJson.get("start");
            endDate = (String) metadataJson.get("end");
        } catch (Exception e) {
            logger.warn("Unable to parse metadata {}", metadata);
            return RestUtil.R.badRequest("Unable to parse metadata");
        }

        if (StringUtils.isBlank(device) || StringUtils.isBlank(startDate) || StringUtils.isBlank(endDate)) {
            logger.warn("Either device, start date or end date were not specified");
            return Response.status(Status.BAD_REQUEST).build();
        }

        Date start;
        try {
            start = new Date(DateTimeSupport.fromUTC(startDate));
        } catch (Exception e) {
            logger.warn("Unable to parse start date {}", startDate);
            return RestUtil.R.badRequest("Unable to parse start date");
        }

        Date end;
        try {
            end = new Date(DateTimeSupport.fromUTC(endDate));
        } catch (Exception e) {
            logger.warn("Unable to parse end date {}", endDate);
            return RestUtil.R.badRequest("Unable to parse end date");
        }

        String rrule = (String) metadataJson.get("rrule");

        String timezone = null;
        String durationString = null;
        if (StringUtils.isNotEmpty(rrule)) {
            try {
                RRule rule = new RRule(rrule);
                rule.validate();
            } catch (Exception e) {
                logger.warn("Unable to parse rrule {}: {}", rrule, e.getMessage());
                return Response.status(Status.BAD_REQUEST).build();
            }

            durationString = (String) metadataJson.get("duration");
            if (StringUtils.isBlank(durationString)) {
                logger.warn("If checking recurrence, must include duration.");
                return Response.status(Status.BAD_REQUEST).build();
            }

            Agent agent = getCaptureAgentStateService().getAgent(device);
            timezone = agent.getConfiguration().getProperty("capture.device.timezone");
            if (StringUtils.isBlank(timezone)) {
                timezone = TimeZone.getDefault().getID();
                logger.warn(
                        "No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.",
                        device, timezone);
            }
        }

        try {
            DublinCoreCatalogList events = null;
            if (StringUtils.isNotEmpty(rrule)) {
                events = getSchedulerService().findConflictingEvents(device, rrule, start, end,
                        Long.parseLong(durationString), timezone);
            } else {
                events = getSchedulerService().findConflictingEvents(device, start, end);
            }
            if (!events.getCatalogList().isEmpty()) {
                List<JValue> eventsJSON = new ArrayList<JValue>();
                for (DublinCoreCatalog event : events.getCatalogList()) {
                    eventsJSON.add(j(f("time", v(event.getFirst(DublinCoreCatalog.PROPERTY_TEMPORAL))),
                            f("title", v(event.getFirst(DublinCoreCatalog.PROPERTY_TITLE)))));
                }

                return conflictJson(a(eventsJSON));
            } else {
                return Response.noContent().build();
            }
        } catch (Exception e) {
            logger.error("Unable to find conflicting events for {}, {}, {}: {}",
                    new String[] { device, startDate, endDate, ExceptionUtils.getStackTrace(e) });
            return RestUtil.R.serverError();
        }
    }

    @POST
    @Path("/new")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @RestQuery(name = "createNewEvent", description = "Creates a new event by the given metadata as JSON and the files in the body", returnDescription = "The workflow identifier", restParameters = {
            @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, reponses = {
                    @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event sucessfully added"),
                    @RestResponse(responseCode = SC_BAD_REQUEST, description = "If the metadata is not set or couldn't be parsed") })
    public Response createNewEvent(@Context HttpServletRequest request) {
        try {
            String result = getIndexService().createEvent(request);
            return Response.status(Status.CREATED).entity(result).build();
        } catch (IllegalArgumentException e) {
            return RestUtil.R.badRequest(e.getMessage());
        } catch (Exception e) {
            return RestUtil.R.serverError();
        }
    }

    /**
     * Get a single event
     *
     * @param id
     *          the mediapackage id
     * @return an event or none if not found wrapped in an option
     * @throws SearchIndexException
     */
    public Opt<Event> getEvent(String id) throws SearchIndexException {
        SearchResult<Event> result = getIndex().getByQuery(
                new EventSearchQuery(getSecurityService().getOrganization().getId(), getSecurityService().getUser())
                        .withIdentifier(id));
        // If the results list if empty, we return already a response.
        if (result.getPageSize() == 0) {
            logger.debug("Didn't find event with id {}", id);
            return Opt.<Event>none();
        }
        Event event = result.getItems()[0].getSource();
        event.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
        return Opt.some(event);
    }

    @GET
    @Path("events.json")
    @Produces(MediaType.APPLICATION_JSON)
    @RestQuery(name = "getevents", description = "Returns all the events as JSON", returnDescription = "All the events as JSON", restParameters = {
            @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
            @RestParameter(name = "sort", description = "The order instructions used to sort the query result. Must be in the form '<field name>:(ASC|DESC)'", isRequired = false, type = STRING),
            @RestParameter(name = "limit", description = "The maximum number of items to return per page.", isRequired = false, type = RestParameter.Type.INTEGER),
            @RestParameter(name = "offset", description = "The page number.", isRequired = false, type = RestParameter.Type.INTEGER) }, reponses = {
                    @RestResponse(description = "Returns all events as JSON", responseCode = HttpServletResponse.SC_OK) })
    public Response getEvents(@QueryParam("id") String id, @QueryParam("commentReason") String reasonFilter,
            @QueryParam("commentResolution") String resolutionFilter, @QueryParam("filter") String filter,
            @QueryParam("sort") String sort, @QueryParam("offset") Integer offset,
            @QueryParam("limit") Integer limit) {

        Option<Integer> optLimit = Option.option(limit);
        Option<Integer> optOffset = Option.option(offset);
        Option<String> optSort = Option.option(trimToNull(sort));
        ArrayList<JValue> eventsList = new ArrayList<JValue>();
        EventSearchQuery query = new EventSearchQuery(getSecurityService().getOrganization().getId(),
                getSecurityService().getUser());

        // If the limit is set to 0, this is not taken into account
        if (optLimit.isSome() && limit == 0) {
            optLimit = Option.none();
        }

        Map<String, String> filters = RestUtils.parseFilter(filter);
        for (String name : filters.keySet()) {
            if (EventListQuery.FILTER_PRESENTERS_NAME.equals(name))
                query.withPresenter(filters.get(name));
            if (EventListQuery.FILTER_CONTRIBUTORS_NAME.equals(name))
                query.withContributor(filters.get(name));
            if (EventListQuery.FILTER_LOCATION_NAME.equals(name))
                query.withLocation(filters.get(name));
            if (EventListQuery.FILTER_TEXT_NAME.equals(name))
                query.withText("*" + filters.get(name) + "*");
            if (EventListQuery.FILTER_SERIES_NAME.equals(name))
                query.withSeriesId(filters.get(name));
            if (EventListQuery.FILTER_STATUS_NAME.equals(name))
                query.withEventStatus(filters.get(name));
            if (EventListQuery.FILTER_COMMENTS_NAME.equals(name)) {
                switch (Comments.valueOf(filters.get(name))) {
                case NONE:
                    query.withComments(false);
                    break;
                case OPEN:
                    query.withOpenComments(true);
                    break;
                case RESOLVED:
                    query.withComments(true);
                    query.withOpenComments(false);
                    break;
                default:
                    logger.info("Unknown comment {}", filters.get(name));
                    return Response.status(SC_BAD_REQUEST).build();
                }
            }
            if (EventListQuery.FILTER_STARTDATE_NAME.equals(name)) {
                try {
                    Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(filters.get(name));
                    query.withStartFrom(fromAndToCreationRange.getA());
                    query.withStartTo(fromAndToCreationRange.getB());
                } catch (IllegalArgumentException e) {
                    return RestUtil.R.badRequest(e.getMessage());
                }
            }
        }

        if (optSort.isSome()) {
            Set<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
            for (SortCriterion criterion : sortCriteria) {
                switch (criterion.getFieldName()) {
                case EventIndexSchema.TITLE:
                    query.sortByTitle(criterion.getOrder());
                    break;
                case EventIndexSchema.PRESENTER:
                    query.sortByPresenter(criterion.getOrder());
                    break;
                case EventIndexSchema.START_DATE:
                case "date":
                    query.sortByStartDate(criterion.getOrder());
                    break;
                case EventIndexSchema.END_DATE:
                    query.sortByEndDate(criterion.getOrder());
                    break;
                case EventIndexSchema.SERIES_NAME:
                    query.sortBySeriesName(criterion.getOrder());
                    break;
                case EventIndexSchema.LOCATION:
                    query.sortByLocation(criterion.getOrder());
                    break;
                case EventIndexSchema.EVENT_STATUS:
                    query.sortByEventStatus(criterion.getOrder());
                    break;
                default:
                    throw new WebApplicationException(Status.BAD_REQUEST);
                }
            }
        }

        // TODO: Add the comment resolution filter to the query
        RESOLUTION resolution = null;
        if (StringUtils.isNotBlank(resolutionFilter)) {
            try {
                resolution = RESOLUTION.valueOf(resolutionFilter);
            } catch (Exception e) {
                logger.warn("Unable to parse comment resolution filter {}", resolutionFilter);
                return Response.status(Status.BAD_REQUEST).build();
            }
        }

        if (optLimit.isSome())
            query.withLimit(optLimit.get());
        if (optOffset.isSome())
            query.withOffset(offset);
        // TODO: Add other filters to the query

        SearchResult<Event> results = null;
        try {
            results = getIndex().getByQuery(query);
        } catch (SearchIndexException e) {
            logger.error("The admin UI Search Index was not able to get the events list: {}", e);
            return RestUtil.R.serverError();
        }

        // If the results list if empty, we return already a response.
        if (results.getPageSize() == 0) {
            logger.debug("No events match the given filters.");
            return okJsonList(eventsList, Opt.nul(offset).or(0), Opt.nul(limit).or(0), 0);
        }

        for (SearchResultItem<Event> item : results.getItems()) {
            Event source = item.getSource();
            source.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
            eventsList.add(eventToJSON(source));
        }

        return okJsonList(eventsList, Opt.nul(offset).or(0), Opt.nul(limit).or(0), results.getHitCount());
    }

    // --

    private void updateCommentCatalog(final Event event, final List<Comment> comments) throws Exception {
        final Opt<MediaPackage> mpOpt = getIndexService().getEventMediapackage(event);
        if (mpOpt.isNone())
            return;

        final SecurityContext securityContext = new SecurityContext(getSecurityService(),
                getSecurityService().getOrganization(), getSecurityService().getUser());
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                securityContext.runInContext(new Effect0() {
                    @Override
                    protected void run() {
                        try {
                            MediaPackage mediaPackage = mpOpt.get();
                            updateMediaPackageCommentCatalog(mediaPackage, comments);
                            switch (getIndexService().getEventSource(event)) {
                            case WORKFLOW:
                                logger.info("Update workflow mediapacakge {} with updated comments catalog.",
                                        event.getIdentifier());
                                WorkflowInstance workflowInstance = getIndexService()
                                        .getCurrentWorkflowInstance(event.getIdentifier());
                                workflowInstance.setMediaPackage(mediaPackage);
                                getIndexService().updateWorkflowInstance(workflowInstance);
                                break;
                            case ARCHIVE:
                                logger.info("Update archive mediapacakge {} with updated comments catalog.",
                                        event.getIdentifier());
                                getArchive().add(mediaPackage);
                                break;
                            default:
                                logger.error("Unkown event source {}!", event.getSource().toString());
                            }
                        } catch (Exception e) {
                            logger.error("Unable to update event {} comment catalog: {}", event.getIdentifier(),
                                    ExceptionUtils.getStackTrace(e));
                        }
                    }
                });
            }
        });
    }

    private URI getCommentUrl(String eventId, long commentId) {
        return UrlSupport.uri(serverUrl, eventId, "comment", Long.toString(commentId));
    }

    private JValue eventToJSON(Event event) {
        List<JField> fields = new ArrayList<JField>();

        fields.add(f("id", v(event.getIdentifier())));
        fields.add(f("title", vN(event.getTitle())));
        fields.add(f("source", vN(event.getSource())));
        fields.add(f("presenters", jsonArrayFromList(event.getPresenters())));
        if (StringUtils.isNotBlank(event.getSeriesId())) {
            String seriesTitle = event.getSeriesName();
            String seriesID = event.getSeriesId();

            fields.add(f("series", j(f("id", vN(seriesID)), f("title", vN(seriesTitle)))));
        }
        fields.add(f("location", vN(event.getLocation())));

        fields.add(f("start_date", vN(event.getRecordingStartDate())));
        if (event.getDuration() != null) {
            try {
                long endTime = DateTimeSupport.fromUTC(event.getRecordingStartDate()) + event.getDuration();
                fields.add(f("end_date", v(DateTimeSupport.toUTC(endTime))));
            } catch (Exception e) {
                logger.error("Unable to parse start time {}", event.getRecordingStartDate());
            }
        }

        String schedulingStatus = event.getSchedulingStatus() == null ? null
                : "EVENTS.EVENTS.SCHEDULING_STATUS." + event.getSchedulingStatus();
        fields.add(f("managedAcl", vN(event.getManagedAcl())));
        fields.add(f("scheduling_status", vN(schedulingStatus)));
        fields.add(f("workflow_state", vN(event.getWorkflowState())));
        fields.add(f("review_status", vN(event.getReviewStatus())));
        fields.add(f("event_status", v(event.getEventStatus())));
        fields.add(f("source", v(getIndexService().getEventSource(event).toString())));
        fields.add(f("has_comments", v(event.hasComments())));
        fields.add(f("has_open_comments", v(event.hasOpenComments())));
        fields.add(f("has_preview", v(event.hasPreview())));
        fields.add(f("publications", j(f("Engage", v("http://engage.localdomain")))));

        return j(fields);
    }

    private static final Fn<Publication, JObjectWrite> publicationToJson = new Fn<Publication, JObjectWrite>() {
        @Override
        public JObjectWrite ap(Publication publication) {
            Opt<String> channel = Opt.nul(PUBLICATION_CHANNELS.get(publication.getChannel()));
            return j(f("name", v(channel.or("EVENTS.EVENTS.DETAILS.GENERAL.CUSTOM"))),
                    f("url", v(publication.getURI().toString())));
        }
    };

    private final Function<Recording, List<Person>> getRecipients = new Function<Recording, List<Person>>() {
        @Override
        public List<Person> apply(Recording a) {
            return a.getStaff();
        }
    };

    private final Fn<Publication, Boolean> internalChannelFilter = new Fn<Publication, Boolean>() {
        @Override
        public Boolean ap(Publication a) {
            if (PublishInternalWorkflowOperationHandler.CHANNEL_ID.equals(a.getChannel()))
                return false;
            return true;
        }
    };

}