1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.external.endpoint;
22
23 import static org.apache.commons.lang3.StringUtils.trimToNull;
24 import static org.opencastproject.external.common.ApiVersion.VERSION_1_11_0;
25 import static org.opencastproject.external.common.ApiVersion.VERSION_1_1_0;
26 import static org.opencastproject.external.common.ApiVersion.VERSION_1_4_0;
27 import static org.opencastproject.external.common.ApiVersion.VERSION_1_7_0;
28 import static org.opencastproject.external.util.SchedulingUtils.SchedulingInfo;
29 import static org.opencastproject.external.util.SchedulingUtils.convertConflictingEvents;
30 import static org.opencastproject.external.util.SchedulingUtils.getConflictingEvents;
31 import static org.opencastproject.index.service.util.JSONUtils.arrayToJsonArray;
32 import static org.opencastproject.index.service.util.JSONUtils.collectionToJsonArray;
33 import static org.opencastproject.index.service.util.JSONUtils.safeString;
34 import static org.opencastproject.util.RestUtil.getEndpointUrl;
35 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
36
37 import org.opencastproject.assetmanager.api.AssetManager;
38 import org.opencastproject.assetmanager.api.AssetManagerException;
39 import org.opencastproject.capture.CaptureParameters;
40 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
41 import org.opencastproject.elasticsearch.api.SearchIndexException;
42 import org.opencastproject.elasticsearch.api.SearchResult;
43 import org.opencastproject.elasticsearch.api.SearchResultItem;
44 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
45 import org.opencastproject.elasticsearch.index.objects.IndexObject;
46 import org.opencastproject.elasticsearch.index.objects.event.Event;
47 import org.opencastproject.elasticsearch.index.objects.event.EventIndexSchema;
48 import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
49 import org.opencastproject.external.common.ApiMediaType;
50 import org.opencastproject.external.common.ApiResponseBuilder;
51 import org.opencastproject.external.common.ApiVersion;
52 import org.opencastproject.external.util.AclUtils;
53 import org.opencastproject.external.util.ExternalMetadataUtils;
54 import org.opencastproject.index.service.api.IndexService;
55 import org.opencastproject.index.service.catalog.adapter.DublinCoreMetadataUtil;
56 import org.opencastproject.index.service.exception.IndexServiceException;
57 import org.opencastproject.index.service.impl.util.EventHttpServletRequest;
58 import org.opencastproject.index.service.impl.util.EventUtils;
59 import org.opencastproject.index.service.util.RequestUtils;
60 import org.opencastproject.index.service.util.RestUtils;
61 import org.opencastproject.ingest.api.IngestException;
62 import org.opencastproject.ingest.api.IngestService;
63 import org.opencastproject.list.impl.EmptyResourceListQuery;
64 import org.opencastproject.mediapackage.Attachment;
65 import org.opencastproject.mediapackage.AudioStream;
66 import org.opencastproject.mediapackage.Catalog;
67 import org.opencastproject.mediapackage.MediaPackage;
68 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
69 import org.opencastproject.mediapackage.MediaPackageException;
70 import org.opencastproject.mediapackage.Publication;
71 import org.opencastproject.mediapackage.Stream;
72 import org.opencastproject.mediapackage.Track;
73 import org.opencastproject.mediapackage.TrackSupport;
74 import org.opencastproject.mediapackage.VideoStream;
75 import org.opencastproject.mediapackage.track.TrackImpl;
76 import org.opencastproject.metadata.dublincore.DublinCore;
77 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
78 import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
79 import org.opencastproject.metadata.dublincore.MetadataField;
80 import org.opencastproject.metadata.dublincore.MetadataJson;
81 import org.opencastproject.metadata.dublincore.MetadataList;
82 import org.opencastproject.metadata.dublincore.MetadataList.Locked;
83 import org.opencastproject.rest.RestConstants;
84 import org.opencastproject.scheduler.api.SchedulerConflictException;
85 import org.opencastproject.scheduler.api.SchedulerException;
86 import org.opencastproject.scheduler.api.SchedulerService;
87 import org.opencastproject.scheduler.api.TechnicalMetadata;
88 import org.opencastproject.security.api.AccessControlEntry;
89 import org.opencastproject.security.api.AccessControlList;
90 import org.opencastproject.security.api.AccessControlParser;
91 import org.opencastproject.security.api.Permissions;
92 import org.opencastproject.security.api.SecurityService;
93 import org.opencastproject.security.api.UnauthorizedException;
94 import org.opencastproject.security.urlsigning.exception.UrlSigningException;
95 import org.opencastproject.security.urlsigning.service.UrlSigningService;
96 import org.opencastproject.systems.OpencastConstants;
97 import org.opencastproject.util.DateTimeSupport;
98 import org.opencastproject.util.NotFoundException;
99 import org.opencastproject.util.RestUtil;
100 import org.opencastproject.util.RestUtil.R;
101 import org.opencastproject.util.UrlSupport;
102 import org.opencastproject.util.data.Tuple;
103 import org.opencastproject.util.doc.rest.RestParameter;
104 import org.opencastproject.util.doc.rest.RestParameter.Type;
105 import org.opencastproject.util.doc.rest.RestQuery;
106 import org.opencastproject.util.doc.rest.RestResponse;
107 import org.opencastproject.util.doc.rest.RestService;
108 import org.opencastproject.util.requests.SortCriterion;
109 import org.opencastproject.workflow.api.WorkflowDatabaseException;
110 import org.opencastproject.workflow.api.WorkflowInstance;
111 import org.opencastproject.workflow.api.WorkflowService;
112
113 import com.google.gson.JsonArray;
114 import com.google.gson.JsonElement;
115 import com.google.gson.JsonNull;
116 import com.google.gson.JsonObject;
117 import com.google.gson.JsonPrimitive;
118
119 import org.apache.commons.fileupload.FileItemIterator;
120 import org.apache.commons.fileupload.FileItemStream;
121 import org.apache.commons.fileupload.FileUploadException;
122 import org.apache.commons.fileupload.servlet.ServletFileUpload;
123 import org.apache.commons.fileupload.util.Streams;
124 import org.apache.commons.lang3.StringUtils;
125 import org.joda.time.DateTime;
126 import org.joda.time.DateTimeZone;
127 import org.json.simple.JSONArray;
128 import org.json.simple.JSONObject;
129 import org.json.simple.parser.JSONParser;
130 import org.json.simple.parser.ParseException;
131 import org.osgi.service.cm.ConfigurationException;
132 import org.osgi.service.cm.ManagedService;
133 import org.osgi.service.component.ComponentContext;
134 import org.osgi.service.component.annotations.Activate;
135 import org.osgi.service.component.annotations.Component;
136 import org.osgi.service.component.annotations.Reference;
137 import org.osgi.service.component.annotations.ReferenceCardinality;
138 import org.osgi.service.component.annotations.ReferencePolicy;
139 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
140 import org.slf4j.Logger;
141 import org.slf4j.LoggerFactory;
142
143 import java.io.IOException;
144 import java.net.URI;
145 import java.text.SimpleDateFormat;
146 import java.time.format.DateTimeParseException;
147 import java.util.ArrayList;
148 import java.util.Date;
149 import java.util.Dictionary;
150 import java.util.HashMap;
151 import java.util.Hashtable;
152 import java.util.List;
153 import java.util.Map;
154 import java.util.Objects;
155 import java.util.Optional;
156 import java.util.TreeMap;
157 import java.util.concurrent.ConcurrentHashMap;
158 import java.util.concurrent.CopyOnWriteArrayList;
159 import java.util.stream.Collectors;
160
161 import javax.servlet.http.HttpServletRequest;
162 import javax.servlet.http.HttpServletResponse;
163 import javax.ws.rs.Consumes;
164 import javax.ws.rs.DELETE;
165 import javax.ws.rs.DefaultValue;
166 import javax.ws.rs.FormParam;
167 import javax.ws.rs.GET;
168 import javax.ws.rs.HeaderParam;
169 import javax.ws.rs.POST;
170 import javax.ws.rs.PUT;
171 import javax.ws.rs.Path;
172 import javax.ws.rs.PathParam;
173 import javax.ws.rs.Produces;
174 import javax.ws.rs.QueryParam;
175 import javax.ws.rs.WebApplicationException;
176 import javax.ws.rs.core.Context;
177 import javax.ws.rs.core.MediaType;
178 import javax.ws.rs.core.Response;
179 import javax.ws.rs.core.Response.Status;
180
181 import io.swagger.v3.oas.annotations.Operation;
182 import io.swagger.v3.oas.annotations.Parameter;
183 import io.swagger.v3.oas.annotations.Parameters;
184 import io.swagger.v3.oas.annotations.enums.ParameterIn;
185 import io.swagger.v3.oas.annotations.responses.ApiResponse;
186 import io.swagger.v3.oas.annotations.responses.ApiResponses;
187 import io.swagger.v3.oas.annotations.tags.Tag;
188
189 @Path("/api/events")
190 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
191 ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
192 ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
193 ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
194 @RestService(name = "externalapievents", title = "External API Events Service", notes = {},
195 abstractText = "Provides resources and operations related to the events")
196 @Tag(name = "External API")
197 @Tag(name = "External API - Events",
198 description = "The events endpoint provides resources and operations related to the events")
199 @Component(
200 immediate = true,
201 service = { EventsEndpoint.class,ManagedService.class },
202 property = {
203 "service.description=External API - Events Endpoint",
204 "opencast.service.type=org.opencastproject.external.events",
205 "opencast.service.path=/api/events"
206 }
207 )
208 @JaxrsResource
209 public class EventsEndpoint implements ManagedService {
210
211 protected static final String URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY = "url.signing.expires.seconds";
212
213
214 protected static final Long DEFAULT_URL_SIGNING_EXPIRE_DURATION = 2 * 60 * 60L;
215
216
217 private static final String PREVIEW_SUBTYPE = "preview.subtype";
218
219
220 private static final String DEFAULT_PREVIEW_SUBTYPE = "preview";
221
222
223 private static final String RETRACT_WORKFLOW = "retract.workflow.id";
224
225
226 private static final String DEFAULT_RETRACT_WORKFLOW = "delete";
227
228
229 private static final Logger logger = LoggerFactory.getLogger(EventsEndpoint.class);
230
231
232 protected String endpointBaseUrl;
233
234 private static long expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION;
235
236 private String previewSubtype = DEFAULT_PREVIEW_SUBTYPE;
237
238 private Map<String, MetadataField> configuredMetadataFields = new TreeMap<>();
239
240 private String retractWorkflowId = DEFAULT_RETRACT_WORKFLOW;
241
242
243 private enum CommentResolution {
244 ALL, UNRESOLVED, RESOLVED;
245 };
246
247
248 private AssetManager assetManager;
249 private ElasticsearchIndex elasticsearchIndex;
250 private IndexService indexService;
251 private IngestService ingestService;
252 private SecurityService securityService;
253 private final List<EventCatalogUIAdapter> catalogUIAdapters = new CopyOnWriteArrayList<>();
254 private final Map<String, List<EventCatalogUIAdapter>> orgCatalogUIAdaptersMap = new ConcurrentHashMap<>();
255 private UrlSigningService urlSigningService;
256 private SchedulerService schedulerService;
257 private CaptureAgentStateService agentStateService;
258 private WorkflowService workflowService;
259
260
261 @Reference
262 public void setAssetManager(AssetManager assetManager) {
263 this.assetManager = assetManager;
264 }
265
266
267 @Reference
268 void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
269 this.elasticsearchIndex = elasticsearchIndex;
270 }
271
272
273 @Reference
274 public void setIndexService(IndexService indexService) {
275 this.indexService = indexService;
276 }
277
278
279 @Reference
280 public void setIngestService(IngestService ingestService) {
281 this.ingestService = ingestService;
282 }
283
284
285 @Reference
286 void setSecurityService(SecurityService securityService) {
287 this.securityService = securityService;
288 }
289
290
291 @Reference
292 public void setUrlSigningService(UrlSigningService urlSigningService) {
293 this.urlSigningService = urlSigningService;
294 }
295
296 public SecurityService getSecurityService() {
297 return securityService;
298 }
299
300 public SchedulerService getSchedulerService() {
301 return schedulerService;
302 }
303
304 @Reference
305 public void setSchedulerService(SchedulerService schedulerService) {
306 this.schedulerService = schedulerService;
307 }
308
309
310 @Reference(
311 cardinality = ReferenceCardinality.MULTIPLE,
312 policy = ReferencePolicy.DYNAMIC,
313 unbind = "removeCatalogUIAdapter"
314 )
315 public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
316 catalogUIAdapters.add(catalogUIAdapter);
317 invalidateOrgCatalogUIAdaptersMapFor(catalogUIAdapter);
318 }
319
320
321 public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
322 catalogUIAdapters.remove(catalogUIAdapter);
323 invalidateOrgCatalogUIAdaptersMapFor(catalogUIAdapter);
324 }
325
326
327
328
329
330
331 private void invalidateOrgCatalogUIAdaptersMapFor(EventCatalogUIAdapter catalogUIAdapter) {
332
333 for (String orgName : orgCatalogUIAdaptersMap.keySet()) {
334 if (catalogUIAdapter.handlesOrganization(orgName)) {
335 orgCatalogUIAdaptersMap.remove(orgName);
336 }
337 }
338 }
339
340
341 public CaptureAgentStateService getAgentStateService() {
342 return agentStateService;
343 }
344
345
346 @Reference
347 public void setAgentStateService(CaptureAgentStateService agentStateService) {
348 this.agentStateService = agentStateService;
349 }
350
351
352 @Reference
353 public void setWorkflowService(WorkflowService workflowService) {
354 this.workflowService = workflowService;
355 }
356
357
358 private List<EventCatalogUIAdapter> getEventCatalogUIAdapters() {
359 return getEventCatalogUIAdapters(getSecurityService().getOrganization().getId());
360 }
361
362 public List<EventCatalogUIAdapter> getEventCatalogUIAdapters(String organization) {
363 List<EventCatalogUIAdapter> cachedCatalogUIAdapters = orgCatalogUIAdaptersMap.computeIfAbsent(organization,
364 org -> new ArrayList<>(catalogUIAdapters.stream()
365 .filter(a -> a.handlesOrganization(org))
366 .collect(Collectors.toList())));
367
368 return new ArrayList<>(cachedCatalogUIAdapters);
369 }
370
371
372 @Activate
373 void activate(ComponentContext cc) {
374 logger.info("Activating External API - Events Endpoint");
375
376 final Tuple<String, String> endpointUrl = getEndpointUrl(cc, OpencastConstants.EXTERNAL_API_URL_ORG_PROPERTY,
377 RestConstants.SERVICE_PATH_PROPERTY);
378 endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
379 logger.debug("Configured service endpoint is {}", endpointBaseUrl);
380 }
381
382
383 @Override
384 public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
385
386 if (properties == null) {
387 properties = new Hashtable<>();
388 logger.debug("No configuration set");
389 }
390
391
392
393 expireSeconds = Long.parseLong(Objects.toString(
394 properties.get(URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY),
395 DEFAULT_URL_SIGNING_EXPIRE_DURATION.toString()));
396 logger.debug("URLs signatures are configured to expire in {}.", DateTimeSupport.humanReadableTime(expireSeconds));
397
398
399
400 previewSubtype = StringUtils.defaultString((String) properties.get(PREVIEW_SUBTYPE), DEFAULT_PREVIEW_SUBTYPE);
401 logger.debug("Preview subtype is '{}'", previewSubtype);
402
403 configuredMetadataFields = DublinCoreMetadataUtil.getDublinCoreProperties(properties);
404
405 retractWorkflowId = StringUtils.defaultString((String) properties.get(RETRACT_WORKFLOW), DEFAULT_RETRACT_WORKFLOW);
406 logger.debug("Retract Workflow is '{}'", retractWorkflowId);
407 }
408
409 public static <T> boolean isNullOrEmpty(List<String> list) {
410 return list == null || list.isEmpty();
411 }
412
413 @GET
414 @Path("{eventId}")
415 @RestQuery(name = "getevent", description = "Returns a single event. By setting the optional sign parameter to true, the method will pre-sign distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of signed URLs when caching this response.", returnDescription = "", pathParameters = {
416 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
417 @RestParameter(name = "sign", isRequired = false, description = "Whether public distribution urls should be signed.", type = Type.BOOLEAN),
418 @RestParameter(name = "withacl", isRequired = false, description = "Whether the acl metadata should be included in the response.", type = Type.BOOLEAN),
419 @RestParameter(name = "withmetadata", isRequired = false, description = "Whether the metadata catalogs should be included in the response.", type = Type.BOOLEAN),
420 @RestParameter(name = "withscheduling", isRequired = false, description = "Whether the scheduling information should be included in the response.", type = Type.BOOLEAN),
421 @RestParameter(name = "withpublications", isRequired = false, description = "Whether the publication ids and urls should be included in the response.", type = Type.BOOLEAN),
422 @RestParameter(name = "includeInternalPublication", isRequired = false, description = "Whether internal publications should be included.", type = Type.BOOLEAN)}, responses = {
423 @RestResponse(description = "The event is returned.", responseCode = HttpServletResponse.SC_OK),
424 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
425 @Operation(summary = "Get a single event", description = "Returns a single event. By setting the optional sign parameter to true, the method will pre-sign distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of signed URLs when caching this response.")
426 public Response getEvent(
427 @HeaderParam("Accept") String acceptHeader,
428 @Parameter(description = "The event id", required = true)
429 @PathParam("eventId") String id,
430 @Parameter(description = "Whether public distribution urls should be signed.")
431 @QueryParam("sign") boolean sign,
432 @Parameter(description = "Whether the acl metadata should be included in the response.")
433 @QueryParam("withacl") Boolean withAcl,
434 @Parameter(description = "Whether the metadata catalogs should be included in the response.")
435 @QueryParam("withmetadata") Boolean withMetadata,
436 @Parameter(description = "Whether the scheduling information should be included in the response.")
437 @QueryParam("withscheduling") Boolean withScheduling,
438 @Parameter(description = "Whether the publication ids and urls should be included in the response.")
439 @QueryParam("withpublications") Boolean withPublications,
440 @Parameter(description = "Whether internal publications should be included.")
441 @QueryParam("includeInternalPublication") Boolean includeInternalPublication)
442 throws Exception {
443 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
444 if (requestedVersion.isSmallerThan(VERSION_1_1_0)) {
445
446 withScheduling = false;
447 }
448 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
449 if (eventOpt.isPresent()) {
450 Event event = eventOpt.get();
451 event.updatePreview(previewSubtype);
452 return ApiResponseBuilder.Json.ok(
453 requestedVersion, eventToJSON(event, withAcl, withMetadata, withScheduling, withPublications, includeInternalPublication, sign, requestedVersion));
454 }
455 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
456 }
457
458 @GET
459 @Path("{eventId}/media")
460 @RestQuery(name = "geteventmedia", description = "Returns media tracks of specific single event.", returnDescription = "", pathParameters = {
461 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
462 @RestResponse(description = "The event's media is returned.", responseCode = HttpServletResponse.SC_OK),
463 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
464 @Operation(summary = "Get media tracks of a single event", description = "Returns media tracks of specific single event.")
465 @Parameters({
466 @Parameter(name = "eventId", description = "The event id", required = true, in = ParameterIn.PATH),
467 @Parameter(name = "Accept", description = "The accept header", required = true, in = ParameterIn.HEADER)
468 })
469 @ApiResponses(value = {
470 @ApiResponse(responseCode = "200", description = "The event's media is returned."),
471 @ApiResponse(responseCode = "404", description = "The specified event does not exist.")
472 })
473 public Response getEventMedia(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
474 throws Exception {
475 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
476 List<TrackImpl> tracks = new ArrayList<>();
477
478 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
479 if (eventOpt.isPresent()) {
480 final MediaPackage mp = indexService.getEventMediapackage(eventOpt.get());
481 for (Track track : mp.getTracks()) {
482 if (track instanceof TrackImpl) {
483 tracks.add((TrackImpl) track);
484 }
485 }
486
487 JsonArray tracksJson = new JsonArray();
488 for (Track track : tracks) {
489 JsonObject trackJson = new JsonObject();
490 if (track.getChecksum() != null)
491 trackJson.addProperty("checksum", track.getChecksum().toString());
492 if (track.getDescription() != null)
493 trackJson.addProperty("description", track.getDescription());
494 if (track.getDuration() != null)
495 trackJson.addProperty("duration", track.getDuration());
496 if (track.getElementDescription() != null)
497 trackJson.addProperty("element-description", track.getElementDescription());
498 if (track.getFlavor() != null)
499 trackJson.addProperty("flavor", track.getFlavor().toString());
500 if (track.getIdentifier() != null)
501 trackJson.addProperty("identifier", track.getIdentifier());
502 if (track.getMimeType() != null)
503 trackJson.addProperty("mimetype", track.getMimeType().toString());
504 trackJson.addProperty("size", track.getSize());
505
506 if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
507 trackJson.addProperty("has_video", track.hasVideo());
508 trackJson.addProperty("has_audio", track.hasAudio());
509 trackJson.addProperty("is_master_playlist", track.isMaster());
510 trackJson.addProperty("is_live", track.isLive());
511 }
512
513 if (track.getStreams() != null) {
514 JsonObject streamsJson = new JsonObject();
515 for (Stream stream : track.getStreams()) {
516 streamsJson.add(stream.getIdentifier(), getJsonStream(stream));
517 }
518 trackJson.add("streams", streamsJson);
519 }
520
521 trackJson.add("tags", arrayToJsonArray(track.getTags()));
522
523 if (track.getURI() != null)
524 trackJson.addProperty("uri", track.getURI().toString());
525
526 tracksJson.add(trackJson);
527 }
528
529 return ApiResponseBuilder.Json.ok(acceptHeader, tracksJson);
530 }
531
532 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
533 }
534
535 @DELETE
536 @Path("{eventId}")
537 @RestQuery(name = "deleteevent", description = "Deletes an event.", returnDescription = "", pathParameters = {
538 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
539 @RestResponse(description = "The event has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
540 @RestResponse(description = "The retraction of publications has started.", responseCode = HttpServletResponse.SC_ACCEPTED),
541 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
542 public Response deleteEvent(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
543 throws SearchIndexException, UnauthorizedException {
544 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
545 if (event.isEmpty()) {
546 return RestUtil.R.notFound(id);
547 }
548 final IndexService.EventRemovalResult result;
549 try {
550 result = indexService.removeEvent(event.get(), retractWorkflowId);
551 } catch (WorkflowDatabaseException e) {
552 logger.error("Workflow database is not reachable. This may be a temporary problem.");
553 return RestUtil.R.serverError();
554 } catch (NotFoundException e) {
555 logger.error("Configured retract workflow not found. Check your configuration.");
556 return RestUtil.R.serverError();
557 }
558 switch (result) {
559 case SUCCESS:
560 return Response.noContent().build();
561 case RETRACTING:
562 return Response.accepted().build();
563 case GENERAL_FAILURE:
564 return Response.serverError().build();
565 case NOT_FOUND:
566 return RestUtil.R.notFound(id);
567 default:
568 throw new RuntimeException("Unknown EventRemovalResult type: " + result.name());
569 }
570 }
571
572 @POST
573 @Path("{eventId}")
574 @RestQuery(name = "updateeventmetadata", description = "Updates an event.", returnDescription = "", pathParameters = {
575 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
576 @RestParameter(name = "acl", isRequired = false, description = "A collection of roles with their possible action", type = Type.STRING),
577 @RestParameter(name = "metadata", isRequired = false, description = "Event metadata as Form param", type = Type.STRING),
578 @RestParameter(name = "scheduling", isRequired = false, description = "Scheduling information as Form param", type = Type.STRING),
579 @RestParameter(name = "presenter", isRequired = false, description = "Presenter movie track", type = Type.FILE),
580 @RestParameter(name = "presentation", isRequired = false, description = "Presentation movie track", type = Type.FILE),
581 @RestParameter(name = "audio", isRequired = false, description = "Audio track", type = Type.FILE),
582 @RestParameter(name = "processing", isRequired = false, description = "Processing instructions task configuration", type = Type.STRING), }, responses = {
583 @RestResponse(description = "The event has been updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
584 @RestResponse(description = "The event could not be updated due to a scheduling conflict.", responseCode = HttpServletResponse.SC_CONFLICT),
585 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
586 public Response updateEventMetadata(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request,
587 @PathParam("eventId") String eventId) {
588 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
589 try {
590 String startDatePattern = configuredMetadataFields.containsKey("startDate") ? configuredMetadataFields.get("startDate").getPattern() : null;
591 String startTimePattern = configuredMetadataFields.containsKey("startTime") ? configuredMetadataFields.get("startTime").getPattern() : null;
592 Optional<Event> eventOpt = indexService.getEvent(eventId, elasticsearchIndex);
593 if (eventOpt.isPresent()) {
594 Event event = eventOpt.get();
595 EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.updateFromHttpServletRequest(event,
596 request, getEventCatalogUIAdapters(), startDatePattern, startTimePattern);
597
598
599 if (eventHttpServletRequest.getMetadataList().isPresent()) {
600 indexService.updateEventMetadata(eventId, eventHttpServletRequest.getMetadataList().get(), elasticsearchIndex);
601 }
602
603 if (eventHttpServletRequest.getAcl().isPresent()) {
604 indexService.updateEventAcl(eventId, eventHttpServletRequest.getAcl().get(), elasticsearchIndex);
605 }
606
607 if (eventHttpServletRequest.getProcessing().isPresent()) {
608
609 if (!event.isScheduledEvent() || event.hasRecordingStarted()) {
610 return RestUtil.R.badRequest("Processing can't be updated for events that are already uploaded.");
611 }
612 JSONObject processing = eventHttpServletRequest.getProcessing().get();
613
614 String workflowId = (String) processing.get("workflow");
615 if (workflowId == null)
616 throw new IllegalArgumentException("No workflow template in metadata");
617
618 Map<String, String> configuration = new HashMap<>();
619 if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
620 configuration = new HashMap<>((JSONObject) eventHttpServletRequest.getProcessing().get().get("configuration"));
621 }
622
623 Optional<Map<String, String>> caMetadataOpt = Optional.empty();
624 Optional<Map<String, String>> workflowConfigOpt = Optional.empty();
625
626 Map<String, String> caMetadata = new HashMap<>(getSchedulerService().getCaptureAgentConfiguration(eventId));
627 if (!workflowId.equals(caMetadata.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION))) {
628 caMetadata.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowId);
629 caMetadataOpt = Optional.of(caMetadata);
630 }
631
632 Map<String, String> oldWorkflowConfig = new HashMap<>(getSchedulerService().getWorkflowConfig(eventId));
633 if (!oldWorkflowConfig.equals(configuration))
634 workflowConfigOpt = Optional.of(configuration);
635
636 if (!caMetadataOpt.isEmpty() || !workflowConfigOpt.isEmpty()) {
637 getSchedulerService().updateEvent(eventId, Optional.empty(), Optional.empty(), Optional.empty(),
638 Optional.empty(), Optional.empty(), workflowConfigOpt, caMetadataOpt);
639 }
640 }
641
642 if (eventHttpServletRequest.getScheduling().isPresent() && !requestedVersion.isSmallerThan(VERSION_1_1_0)) {
643
644 Optional<Response> clientError = updateSchedulingInformation(
645 eventHttpServletRequest.getScheduling().get(), eventId, requestedVersion, false);
646 if (clientError.isPresent()) {
647 return clientError.get();
648 }
649 }
650
651 return Response.noContent().build();
652 }
653 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", eventId);
654 } catch (NotFoundException e) {
655 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", eventId);
656 } catch (UnauthorizedException e) {
657 return Response.status(Status.UNAUTHORIZED).build();
658 } catch (IllegalArgumentException e) {
659 logger.debug("Unable to update event '{}'", eventId, e);
660 return RestUtil.R.badRequest(e.getMessage());
661 } catch (IndexServiceException e) {
662 logger.error("Unable to get multi part fields or file for event '{}'", eventId, e);
663 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
664 } catch (SearchIndexException e) {
665 logger.error("Unable to update event '{}'", eventId, e);
666 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
667 } catch (Exception e) {
668 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
669 }
670 }
671
672 @POST
673 @Path("/")
674 @Consumes(MediaType.MULTIPART_FORM_DATA)
675 @RestQuery(name = "createevent", description = "Creates an event by sending metadata, access control list, processing instructions and files in a multipart request.", returnDescription = "", restParameters = {
676 @RestParameter(name = "acl", isRequired = false, description = "A collection of roles with their possible action", type = STRING),
677 @RestParameter(name = "metadata", description = "Event metadata as Form param", isRequired = false, type = STRING),
678 @RestParameter(name = "scheduling", description = "Scheduling information as Form param", isRequired = false, type = STRING),
679 @RestParameter(name = "presenter", description = "Presenter movie track", isRequired = false, type = Type.FILE),
680 @RestParameter(name = "presentation", description = "Presentation movie track", isRequired = false, type = Type.FILE),
681 @RestParameter(name = "audio", description = "Audio track", isRequired = false, type = Type.FILE),
682 @RestParameter(name = "processing", description = "Processing instructions task configuration", isRequired = false, type = STRING) }, responses = {
683 @RestResponse(description = "A new event is created and its identifier is returned in the Location header.", responseCode = HttpServletResponse.SC_CREATED),
684 @RestResponse(description = "The event could not be created due to a scheduling conflict.", responseCode = HttpServletResponse.SC_CONFLICT),
685 @RestResponse(description = "The request is invalid or inconsistent..", responseCode = HttpServletResponse.SC_BAD_REQUEST) })
686 public Response createNewEvent(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request) {
687 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
688 try {
689 String startDatePattern = configuredMetadataFields.containsKey("startDate") ? configuredMetadataFields.get("startDate").getPattern() : null;
690 String startTimePattern = configuredMetadataFields.containsKey("startTime") ? configuredMetadataFields.get("startTime").getPattern() : null;
691 EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.createFromHttpServletRequest(request,
692 ingestService, getEventCatalogUIAdapters(), startDatePattern, startTimePattern);
693
694
695 if (eventHttpServletRequest.getScheduling().isPresent() && !requestedVersion.isSmallerThan(VERSION_1_1_0)) {
696
697 return scheduleNewEvent(eventHttpServletRequest, eventHttpServletRequest.getScheduling().get(), requestedVersion);
698 }
699
700 JSONObject source = new JSONObject();
701 source.put("type", "UPLOAD");
702 eventHttpServletRequest.setSource(source);
703 String eventId = indexService.createEvent(eventHttpServletRequest);
704 JsonObject json = new JsonObject();
705 json.addProperty("identifier", eventId);
706 return ApiResponseBuilder.Json.created(requestedVersion, URI.create(getEventUrl(eventId)), json);
707 } catch (IllegalArgumentException | DateTimeParseException e) {
708 logger.debug("Unable to create event", e);
709 return RestUtil.R.badRequest(e.getMessage());
710 } catch (SchedulerException | IndexServiceException e) {
711 if (e.getCause() != null && e.getCause() instanceof NotFoundException
712 || e.getCause() instanceof IllegalArgumentException) {
713 logger.debug("Unable to create event", e);
714 return RestUtil.R.badRequest(e.getCause().getMessage());
715 } else {
716 logger.error("Unable to create event", e);
717 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
718 }
719 } catch (Exception e) {
720 logger.error("Unable to create event", e);
721 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
722 }
723 }
724
725 private Response scheduleNewEvent(EventHttpServletRequest request, JSONObject scheduling, ApiVersion requestedVersion)
726 throws MediaPackageException, IOException, IngestException, SchedulerException,
727 NotFoundException, UnauthorizedException, SearchIndexException, java.text.ParseException {
728
729 final SchedulingInfo schedulingInfo = SchedulingInfo.of(scheduling);
730 final JSONObject source = schedulingInfo.toSource();
731 request.setSource(source);
732
733 try {
734 final String eventId = indexService.createEvent(request);
735
736 if (StringUtils.isEmpty(eventId)) {
737 return RestUtil.R.badRequest("The date range provided did not include any events");
738 }
739
740 if (eventId.contains(",")) {
741
742 JsonArray eventArray = new JsonArray();
743 for (String id : eventId.split(",")) {
744 JsonObject eventObj = new JsonObject();
745 eventObj.addProperty("identifier", id);
746 eventArray.add(eventObj);
747 }
748 return ApiResponseBuilder.Json.ok(requestedVersion, eventArray);
749 }
750
751 JsonObject eventJson = new JsonObject();
752 eventJson.addProperty("identifier", eventId);
753 return ApiResponseBuilder.Json.created(requestedVersion, URI.create(getEventUrl(eventId)), eventJson);
754 } catch (SchedulerConflictException e) {
755 List<MediaPackage> conflictingEvents =
756 getConflictingEvents(schedulingInfo, agentStateService, schedulerService);
757 logger.debug("Client tried to schedule conflicting event(s).");
758 JsonArray conflictArray = new JsonArray();
759 for (JsonObject conflict : convertConflictingEvents(
760 Optional.empty(), conflictingEvents, indexService, elasticsearchIndex)) {
761 conflictArray.add(conflict);
762 }
763 return ApiResponseBuilder.Json.conflict(requestedVersion, conflictArray);
764 }
765 }
766
767 @GET
768 @Path("/")
769 @RestQuery(name = "getevents", description = "Returns a list of events. By setting the optional sign parameter to true, the method will pre-sign distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of signed URLs when caching this response.", returnDescription = "", restParameters = {
770 @RestParameter(name = "sign", isRequired = false, description = "Whether public distribution urls should be signed.", type = Type.BOOLEAN),
771 @RestParameter(name = "withacl", isRequired = false, description = "Whether the acl metadata should be included in the response.", type = Type.BOOLEAN),
772 @RestParameter(name = "withmetadata", isRequired = false, description = "Whether the metadata catalogs should be included in the response.", type = Type.BOOLEAN),
773 @RestParameter(name = "withscheduling", isRequired = false, description = "Whether the scheduling information should be included in the response.", type = Type.BOOLEAN),
774 @RestParameter(name = "withpublications", isRequired = false, description = "Whether the publication ids and urls should be included in the response.", type = Type.BOOLEAN),
775 @RestParameter(name = "includeInternalPublication", description = "Whether internal publications should be included.", isRequired = false, type = Type.BOOLEAN),
776 @RestParameter(name = "onlyWithWriteAccess", isRequired = false, description = "Whether only to get the events to which we have write access.", type = Type.BOOLEAN),
777 @RestParameter(name = "filter", isRequired = false, description = "Usage [Filter Name]:[Value to Filter With]. Multiple filters can be used by combining them with commas \",\". Available Filters: presenters, contributors, location, textFilter, series, subject. If API ver > 1.1.0 also: identifier, title, description, series_name, language, created, license, rightsholder, is_part_of, source, status, agent_id, start, technical_start.", type = STRING),
778 @RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting criteria. In the comma seperated list each type of sorting is specified as a pair such as: <Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or descending order and is mandatory.", isRequired = false, type = STRING),
779 @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.", isRequired = false, type = RestParameter.Type.INTEGER),
780 @RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false, type = RestParameter.Type.INTEGER) }, responses = {
781 @RestResponse(description = "A (potentially empty) list of events is returned.", responseCode = HttpServletResponse.SC_OK) })
782 public Response getEvents(@HeaderParam("Accept") String acceptHeader, @QueryParam("id") String id,
783 @QueryParam("commentReason") String reasonFilter, @QueryParam("commentResolution") String resolutionFilter,
784 @QueryParam("filter") List<String> filter, @QueryParam("sort") String sort, @QueryParam("offset") Integer offset,
785 @QueryParam("limit") Integer limit, @QueryParam("sign") boolean sign, @QueryParam("withacl") Boolean withAcl,
786 @QueryParam("withmetadata") Boolean withMetadata, @QueryParam("withscheduling") Boolean withScheduling,
787 @QueryParam("onlyWithWriteAccess") Boolean onlyWithWriteAccess, @QueryParam("withpublications") Boolean withPublications, @QueryParam("includeInternalPublication") Boolean includeInternalPublication) {
788 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
789 if (requestedVersion.isSmallerThan(VERSION_1_1_0)) {
790
791 withScheduling = false;
792 }
793
794 Optional<Integer> optLimit = Optional.ofNullable(limit);
795 Optional<Integer> optOffset = Optional.ofNullable(offset);
796 Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
797 EventSearchQuery query = new EventSearchQuery(getSecurityService().getOrganization().getId(),
798 getSecurityService().getUser());
799
800 if (optLimit.isPresent() && limit == 0) {
801 optLimit = Optional.empty();
802 }
803
804
805 List<IndexObject> allEvents = new ArrayList<>();
806
807 if (!isNullOrEmpty(filter)) {
808
809 if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_5_0)) {
810 filter = filter.subList(0,1);
811 }
812 for (String filterPart : filter) {
813
814
815 for (String f : filterPart.split(",")) {
816 String[] filterTuple = f.split(":");
817 if (filterTuple.length < 2) {
818 logger.debug("No value for filter {} in filters list: {}", filterTuple[0], filter);
819 continue;
820 }
821
822 String name = filterTuple[0];
823 String value;
824
825 if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_1_0)) {
826
827 value = f.substring(name.length() + 1);
828 } else {
829 value = filterTuple[1];
830 }
831
832 if ("presenters".equals(name)) {
833 query.withPresenter(value);
834 } else if ("contributors".equals(name)) {
835 query.withContributor(value);
836 } else if ("location".equals(name)) {
837 query.withLocation(value);
838 } else if ("textFilter".equals(name)) {
839 query.withText(value);
840 } else if ("series".equals(name)) {
841 query.withSeriesId(value);
842 } else if ("subject".equals(name)) {
843 query.withSubject(value);
844 } else if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_1_0)) {
845
846 if ("identifier".equals(name)) {
847 query.withIdentifier(value);
848 } else if ("title".equals(name)) {
849 query.withTitle(value);
850 } else if ("description".equals(name)) {
851 query.withDescription(value);
852 } else if ("series_name".equals(name)) {
853 query.withSeriesName(value);
854 } else if ("language".equals(name)) {
855 query.withLanguage(value);
856 } else if ("created".equals(name)) {
857 query.withCreated(value);
858 } else if ("license".equals(name)) {
859 query.withLicense(value);
860 } else if ("rightsholder".equals(name)) {
861 query.withRights(value);
862 } else if ("is_part_of".equals(name)) {
863 query.withSeriesId(value);
864 } else if ("source".equals(name)) {
865 query.withSource(value);
866 } else if ("status".equals(name)) {
867 query.withEventStatus(value);
868 } else if ("agent_id".equals(name)) {
869 query.withAgentId(value);
870 } else if ("start".equals(name)) {
871 try {
872 Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(value);
873 query.withStartFrom(fromAndToCreationRange.getA());
874 query.withStartTo(fromAndToCreationRange.getB());
875 } catch (Exception e) {
876 return RestUtil.R
877 .badRequest(String.format("Filter 'start' could not be parsed: %s", e.getMessage()));
878
879 }
880 } else if ("technical_start".equals(name)) {
881 try {
882 Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(value);
883 query.withTechnicalStartFrom(fromAndToCreationRange.getA());
884 query.withTechnicalStartTo(fromAndToCreationRange.getB());
885 } catch (Exception e) {
886 return RestUtil.R
887 .badRequest(String.format("Filter 'technical_start' could not be parsed: %s", e.getMessage()));
888
889 }
890 } else {
891 logger.warn("Unknown filter criteria {}", name);
892 return RestUtil.R.badRequest(String.format("Unknown filter criterion in request: %s", name));
893
894 }
895 }
896 }
897
898 if (optSort.isPresent()) {
899 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
900 for (SortCriterion criterion : sortCriteria) {
901
902 switch (criterion.getFieldName()) {
903 case EventIndexSchema.TITLE:
904 query.sortByTitle(criterion.getOrder());
905 break;
906 case EventIndexSchema.PRESENTER:
907 query.sortByPresenter(criterion.getOrder());
908 break;
909 case EventIndexSchema.TECHNICAL_START:
910 case "technical_date":
911 query.sortByTechnicalStartDate(criterion.getOrder());
912 break;
913 case EventIndexSchema.TECHNICAL_END:
914 query.sortByTechnicalEndDate(criterion.getOrder());
915 break;
916 case EventIndexSchema.START_DATE:
917 case "date":
918 query.sortByStartDate(criterion.getOrder());
919 break;
920 case EventIndexSchema.END_DATE:
921 query.sortByEndDate(criterion.getOrder());
922 break;
923 case EventIndexSchema.WORKFLOW_STATE:
924 query.sortByWorkflowState(criterion.getOrder());
925 break;
926 case EventIndexSchema.SERIES_NAME:
927 query.sortBySeriesName(criterion.getOrder());
928 break;
929 case EventIndexSchema.LOCATION:
930 query.sortByLocation(criterion.getOrder());
931 break;
932
933 case "review_status":
934 case "scheduling_status":
935 break;
936 default:
937 return RestUtil.R.badRequest(String.format("Unknown sort criterion in request: %s", criterion.getFieldName()));
938 }
939 }
940 }
941
942
943 if (StringUtils.isNotBlank(resolutionFilter)) {
944 try {
945 CommentResolution.valueOf(resolutionFilter);
946 } catch (Exception e) {
947 logger.debug("Unable to parse comment resolution filter {}", resolutionFilter);
948 return Response.status(Status.BAD_REQUEST).build();
949 }
950 }
951
952 if (optLimit.isPresent())
953 query.withLimit(optLimit.get());
954 if (optOffset.isPresent())
955 query.withOffset(offset);
956
957
958 SearchResult<Event> results = null;
959 try {
960 results = elasticsearchIndex.getByQuery(query);
961 } catch (SearchIndexException e) {
962 logger.error("The External Search Index was not able to get the events list", e);
963 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
964 }
965
966 SearchResultItem<Event>[] items = results.getItems();
967 List<IndexObject> events = new ArrayList<>();
968 for (SearchResultItem<Event> item : items) {
969 Event source = item.getSource();
970 source.updatePreview(previewSubtype);
971 events.add(source);
972 }
973
974 allEvents.addAll(events);
975 }
976 } else {
977 if (optSort.isPresent()) {
978 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
979 for (SortCriterion criterion : sortCriteria) {
980
981 switch (criterion.getFieldName()) {
982 case EventIndexSchema.TITLE:
983 query.sortByTitle(criterion.getOrder());
984 break;
985 case EventIndexSchema.PRESENTER:
986 query.sortByPresenter(criterion.getOrder());
987 break;
988 case EventIndexSchema.TECHNICAL_START:
989 case "technical_date":
990 query.sortByTechnicalStartDate(criterion.getOrder());
991 break;
992 case EventIndexSchema.TECHNICAL_END:
993 query.sortByTechnicalEndDate(criterion.getOrder());
994 break;
995 case EventIndexSchema.START_DATE:
996 case "date":
997 query.sortByStartDate(criterion.getOrder());
998 break;
999 case EventIndexSchema.END_DATE:
1000 query.sortByEndDate(criterion.getOrder());
1001 break;
1002 case EventIndexSchema.WORKFLOW_STATE:
1003 query.sortByWorkflowState(criterion.getOrder());
1004 break;
1005 case EventIndexSchema.SERIES_NAME:
1006 query.sortBySeriesName(criterion.getOrder());
1007 break;
1008 case EventIndexSchema.LOCATION:
1009 query.sortByLocation(criterion.getOrder());
1010 break;
1011
1012 case "review_status":
1013 case "scheduling_status":
1014 break;
1015 default:
1016 return RestUtil.R.badRequest(String.format("Unknown sort criterion in request: %s", criterion.getFieldName()));
1017 }
1018 }
1019 }
1020
1021
1022 if (StringUtils.isNotBlank(resolutionFilter)) {
1023 try {
1024 CommentResolution.valueOf(resolutionFilter);
1025 } catch (Exception e) {
1026 logger.debug("Unable to parse comment resolution filter {}", resolutionFilter);
1027 return Response.status(Status.BAD_REQUEST).build();
1028 }
1029 }
1030
1031 if (optLimit.isPresent())
1032 query.withLimit(optLimit.get());
1033 if (optOffset.isPresent())
1034 query.withOffset(offset);
1035
1036 if (onlyWithWriteAccess != null && onlyWithWriteAccess) {
1037 query.withoutActions();
1038 query.withAction(Permissions.Action.WRITE);
1039 }
1040
1041
1042 SearchResult<Event> results = null;
1043 try {
1044 results = elasticsearchIndex.getByQuery(query);
1045 } catch (SearchIndexException e) {
1046 logger.error("The External Search Index was not able to get the events list", e);
1047 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1048 }
1049
1050 SearchResultItem<Event>[] items = results.getItems();
1051 List<IndexObject> events = new ArrayList<>();
1052 for (SearchResultItem<Event> item : items) {
1053 Event source = item.getSource();
1054 source.updatePreview(previewSubtype);
1055 events.add(source);
1056 }
1057
1058 allEvents.addAll(events);
1059 }
1060 try {
1061 return getJsonEvents(
1062 acceptHeader, allEvents, withAcl, withMetadata, withScheduling, withPublications, includeInternalPublication, sign, requestedVersion);
1063 } catch (Exception e) {
1064 logger.error("Unable to get events", e);
1065 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1066 }
1067 }
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091 protected Response getJsonEvents(String acceptHeader, List<IndexObject> events, Boolean withAcl, Boolean withMetadata,
1092 Boolean withScheduling, Boolean withPublications, Boolean includeInternalPublication, Boolean withSignedUrls, ApiVersion requestedVersion)
1093 throws IndexServiceException, UnauthorizedException, SchedulerException {
1094 JsonArray eventsArray = new JsonArray();
1095 for (IndexObject item : events) {
1096 JsonObject jsonEvent = eventToJSON((Event) item, withAcl, withMetadata, withScheduling, withPublications,
1097 includeInternalPublication, withSignedUrls, requestedVersion);
1098 eventsArray.add(jsonEvent);
1099 }
1100
1101 return ApiResponseBuilder.Json.ok(requestedVersion, eventsArray);
1102 }
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125 protected JsonObject eventToJSON(Event event, Boolean withAcl, Boolean withMetadata, Boolean withScheduling,
1126 Boolean withPublications, Boolean includeInternalPublication, Boolean withSignedUrls,
1127 ApiVersion requestedVersion) throws IndexServiceException, SchedulerException, UnauthorizedException {
1128 JsonObject json = new JsonObject();
1129
1130 if (event.getArchiveVersion() != null)
1131 json.addProperty("archive_version", event.getArchiveVersion());
1132 json.addProperty("created", safeString(event.getCreated()));
1133 json.addProperty("creator", safeString(event.getCreator()));
1134 json.add("contributor", collectionToJsonArray(event.getContributors()));
1135 json.addProperty("description", safeString(event.getDescription()));
1136 json.addProperty("has_previews", event.hasPreview());
1137 json.addProperty("identifier", safeString(event.getIdentifier()));
1138 json.addProperty("location", safeString(event.getLocation()));
1139 json.add("presenter", collectionToJsonArray(event.getPresenters()));
1140
1141 if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
1142 json.addProperty("language", safeString(event.getLanguage()));
1143 json.addProperty("rightsholder", safeString(event.getRights()));
1144 json.addProperty("license", safeString(event.getLicense()));
1145 json.addProperty("is_part_of", safeString(event.getSeriesId()));
1146 json.addProperty("series", safeString(event.getSeriesName()));
1147 json.addProperty("source", safeString(event.getSource()));
1148 json.addProperty("status", safeString(event.getEventStatus()));
1149 }
1150
1151 JsonArray publicationIds = new JsonArray();
1152 if (event.getPublications() != null) {
1153 for (Publication publication : event.getPublications()) {
1154 publicationIds.add(new JsonPrimitive(publication.getChannel()));
1155 }
1156 }
1157 json.add("publication_status", publicationIds);
1158 json.addProperty("processing_state", safeString(event.getWorkflowState()));
1159
1160 if (requestedVersion.isSmallerThan(VERSION_1_4_0)) {
1161 json.addProperty("start", safeString(event.getTechnicalStartTime()));
1162 if (event.getTechnicalEndTime() != null) {
1163 long duration = new DateTime(event.getTechnicalEndTime()).getMillis()
1164 - new DateTime(event.getTechnicalStartTime()).getMillis();
1165 json.addProperty("duration", duration);
1166 }
1167 } else {
1168 json.addProperty("start", safeString(event.getRecordingStartDate()));
1169 if (event.getDuration() != null) {
1170 json.addProperty("duration", event.getDuration());
1171 } else {
1172 json.add("duration", JsonNull.INSTANCE);
1173 }
1174 }
1175
1176 if (StringUtils.trimToNull(event.getSubject()) != null) {
1177 json.add("subjects", splitSubjectIntoArray(event.getSubject()));
1178 } else {
1179 json.add("subjects", new JsonArray());
1180 }
1181
1182 json.addProperty("title", safeString(event.getTitle()));
1183
1184 if (withAcl != null && withAcl) {
1185 AccessControlList acl = getAclFromEvent(event);
1186 json.add("acl", AclUtils.serializeAclToJson(acl));
1187 }
1188
1189 if (withMetadata != null && withMetadata) {
1190 try {
1191 Optional<MetadataList> metadata = getEventMetadata(event);
1192 if (metadata.isPresent()) {
1193 json.add("metadata", MetadataJson.listToJson(metadata.get(), true));
1194 }
1195 } catch (Exception e) {
1196 logger.error("Unable to get metadata for event '{}'", event.getIdentifier(), e);
1197 throw new IndexServiceException("Unable to add metadata to event", e);
1198 }
1199 }
1200
1201 if (withScheduling != null && withScheduling) {
1202 json.add("scheduling", SchedulingInfo.of(event.getIdentifier(), schedulerService).toJson());
1203 }
1204
1205 if (withPublications != null && withPublications) {
1206 List<JsonObject> publications = getPublications(event, withSignedUrls, includeInternalPublication, requestedVersion);
1207 JsonArray pubDetails = new JsonArray();
1208 for (JsonObject pub : publications) {
1209 pubDetails.add(pub);
1210 }
1211 json.add("publications", pubDetails);
1212 }
1213
1214 return json;
1215 }
1216
1217 private JsonArray splitSubjectIntoArray(final String subject) {
1218 JsonArray array = new JsonArray();
1219 if (subject != null && !subject.trim().isEmpty()) {
1220 for (String part : subject.split(",")) {
1221 array.add(new JsonPrimitive(part.trim()));
1222 }
1223 }
1224 return array;
1225 }
1226
1227 @GET
1228 @Path("{eventId}/acl")
1229 @RestQuery(name = "geteventacl", description = "Returns an event's access policy.", returnDescription = "", pathParameters = {
1230 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
1231 @RestResponse(description = "The access control list for the specified event is returned.", responseCode = HttpServletResponse.SC_OK),
1232 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1233 public Response getEventAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
1234 throws Exception {
1235 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1236 if (eventOpt.isPresent()) {
1237 AccessControlList acl = getAclFromEvent(eventOpt.get());
1238 return ApiResponseBuilder.Json.ok(acceptHeader, AclUtils.serializeAclToJson(acl));
1239 }
1240 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1241 }
1242
1243 @PUT
1244 @Path("{eventId}/acl")
1245 @RestQuery(name = "updateeventacl", description = "Update an event's access policy.", returnDescription = "", pathParameters = {
1246 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
1247 @RestParameter(name = "acl", isRequired = true, description = "Access policy", type = STRING) }, responses = {
1248 @RestResponse(description = "The access control list for the specified event is updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1249 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1250 public Response updateEventAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1251 @FormParam("acl") String acl) throws Exception {
1252 if (indexService.getEvent(id, elasticsearchIndex).isPresent()) {
1253 AccessControlList accessControlList;
1254 try {
1255 accessControlList = AclUtils.deserializeJsonToAcl(acl, false);
1256 } catch (ParseException e) {
1257 logger.debug("Unable to update event acl to '{}'", acl, e);
1258 return R.badRequest(String.format("Unable to parse acl '%s' because '%s'", acl, e.getMessage()));
1259 } catch (IllegalArgumentException e) {
1260 logger.debug("Unable to update event acl to '{}'", acl, e);
1261 return R.badRequest(e.getMessage());
1262 }
1263 try {
1264 accessControlList = indexService.updateEventAcl(id, accessControlList, elasticsearchIndex);
1265 } catch (IllegalArgumentException e) {
1266 logger.error("Unable to update event '{}' acl with '{}'", id, acl, e);
1267 return Response.status(Status.FORBIDDEN).build();
1268 }
1269 return Response.noContent().build();
1270 } else {
1271 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1272 }
1273 }
1274
1275 @POST
1276 @Path("{eventId}/acl/{action}")
1277 @RestQuery(name = "addeventace", description = "Grants permission to execute action on the specified event to any user with role role. Note that this is a convenience method to avoid having to build and post a complete access control list.", returnDescription = "", pathParameters = {
1278 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
1279 @RestParameter(name = "action", description = "The action that is allowed to be executed", isRequired = true, type = STRING) }, restParameters = {
1280 @RestParameter(name = "role", isRequired = true, description = "The role that is granted permission", type = STRING) }, responses = {
1281 @RestResponse(description = "The permission has been created in the access control list of the specified event.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1282 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1283 public Response addEventAce(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1284 @PathParam("action") String action, @FormParam("role") String role) throws Exception {
1285 List<AccessControlEntry> entries = new ArrayList<>();
1286 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1287 if (eventOpt.isPresent()) {
1288 AccessControlList accessControlList = getAclFromEvent(eventOpt.get());
1289 AccessControlEntry newAce = new AccessControlEntry(role, action, true);
1290 boolean alreadyInAcl = false;
1291 for (AccessControlEntry ace : accessControlList.getEntries()) {
1292 if (ace.equals(newAce)) {
1293
1294 entries = accessControlList.getEntries();
1295 alreadyInAcl = true;
1296 break;
1297 } else if (ace.getAction().equals(newAce.getAction()) && ace.getRole().equals(newAce.getRole())
1298 && !ace.isAllow()) {
1299 entries.add(newAce);
1300 alreadyInAcl = true;
1301 } else {
1302 entries.add(ace);
1303 }
1304 }
1305
1306 if (!alreadyInAcl) {
1307 entries.add(newAce);
1308 }
1309
1310 AccessControlList withNewAce = new AccessControlList(entries);
1311 try {
1312 withNewAce = indexService.updateEventAcl(id, withNewAce, elasticsearchIndex);
1313 } catch (IllegalArgumentException e) {
1314 logger.error("Unable to update event '{}' acl entry with action '{}' and role '{}'", id, action, role, e);
1315 return Response.status(Status.FORBIDDEN).build();
1316 }
1317 return Response.noContent().build();
1318 }
1319 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1320 }
1321
1322 @DELETE
1323 @Path("{eventId}/acl/{action}/{role}")
1324 @RestQuery(name = "deleteeventace", description = "Revokes permission to execute action on the specified event from any user with role role.", returnDescription = "", pathParameters = {
1325 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
1326 @RestParameter(name = "action", description = "The action that is no longer allowed to be executed", isRequired = true, type = STRING),
1327 @RestParameter(name = "role", description = "The role that is no longer granted permission", isRequired = true, type = STRING) }, responses = {
1328 @RestResponse(description = "The permission has been revoked from the access control list of the specified event.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1329 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1330 public Response deleteEventAce(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1331 @PathParam("action") String action, @PathParam("role") String role) throws Exception {
1332 List<AccessControlEntry> entries = new ArrayList<>();
1333 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1334 if (eventOpt.isPresent()) {
1335 AccessControlList accessControlList = getAclFromEvent(eventOpt.get());
1336 boolean foundDelete = false;
1337 for (AccessControlEntry ace : accessControlList.getEntries()) {
1338 if (ace.getAction().equals(action) && ace.getRole().equals(role)) {
1339 foundDelete = true;
1340 } else {
1341 entries.add(ace);
1342 }
1343 }
1344
1345 if (!foundDelete) {
1346 return ApiResponseBuilder.notFound("Unable to find an access control entry with action '%s' and role '%s'", action,
1347 role);
1348 }
1349
1350 AccessControlList withoutDeleted = new AccessControlList(entries);
1351 try {
1352 withoutDeleted = indexService.updateEventAcl(id, withoutDeleted, elasticsearchIndex);
1353 } catch (IllegalArgumentException e) {
1354 logger.error("Unable to delete event's '{}' acl entry with action '{}' and role '{}'", id, action, role, e);
1355 return Response.status(Status.FORBIDDEN).build();
1356 }
1357 return Response.noContent().build();
1358 }
1359 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1360 }
1361
1362 @GET
1363 @Path("{eventId}/metadata")
1364 @RestQuery(name = "geteventmetadata", description = "Returns the event's metadata of the specified type. For a metadata catalog there is the flavor such as 'dublincore/episode' and this is the unique type.", returnDescription = "", pathParameters = {
1365 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
1366 @RestParameter(name = "type", isRequired = false, description = "The type of metadata to get", type = STRING) }, responses = {
1367 @RestResponse(description = "The metadata collection is returned.", responseCode = HttpServletResponse.SC_OK),
1368 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1369 public Response getAllEventMetadata(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1370 @QueryParam("type") String type) throws Exception {
1371 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
1372 if (StringUtils.trimToNull(type) == null) {
1373 Optional<MetadataList> metadataList = getEventMetadataById(id);
1374 if (metadataList.isPresent()) {
1375 MetadataList actualList = metadataList.get();
1376
1377
1378 final DublinCoreMetadataCollection collection = actualList.getMetadataByFlavor("dublincore/episode");
1379 final boolean withOrderedText = collection == null;
1380 if (collection != null) {
1381 convertStartDateTimeToApiV1(collection);
1382 }
1383
1384 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.listToJson(actualList, withOrderedText));
1385 }
1386 else
1387 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1388 } else {
1389 return getEventMetadataByType(id, type, requestedVersion);
1390 }
1391 }
1392
1393 private void convertStartDateTimeToApiV1(DublinCoreMetadataCollection collection) throws java.text.ParseException {
1394
1395 if (!collection.getOutputFields().containsKey("startDate")) return;
1396
1397 MetadataField oldStartDateField = collection.getOutputFields().get("startDate");
1398 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(oldStartDateField.getPattern());
1399 Date startDate = sdf.parse((String) oldStartDateField.getValue());
1400
1401 if (configuredMetadataFields.containsKey("startDate")) {
1402 MetadataField startDateField = configuredMetadataFields.get("startDate");
1403 final String pattern = startDateField.getPattern() == null ? "yyyy-MM-dd" : startDateField.getPattern();
1404 startDateField = new MetadataField(startDateField);
1405 startDateField.setPattern(pattern);
1406 sdf.applyPattern(startDateField.getPattern());
1407 startDateField.setValue(sdf.format(startDate));
1408 collection.removeField(oldStartDateField);
1409 collection.addField(startDateField);
1410 }
1411
1412 if (configuredMetadataFields.containsKey("startTime")) {
1413 MetadataField startTimeField = configuredMetadataFields.get("startTime");
1414 final String pattern = startTimeField.getPattern() == null ? "HH:mm" : startTimeField.getPattern();
1415 startTimeField = new MetadataField(startTimeField);
1416 startTimeField.setPattern(pattern);
1417 sdf.applyPattern(startTimeField.getPattern());
1418 startTimeField.setValue(sdf.format(startDate));
1419 collection.addField(startTimeField);
1420 }
1421 }
1422
1423 protected Optional<MetadataList> getEventMetadataById(String id) throws IndexServiceException, Exception {
1424 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1425 if (eventOpt.isPresent()) {
1426 return getEventMetadata(eventOpt.get());
1427 }
1428 return Optional.<MetadataList> empty();
1429 }
1430
1431 protected Optional<MetadataList> getEventMetadata(Event event) throws IndexServiceException, Exception {
1432 MetadataList metadataList = new MetadataList();
1433 List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
1434 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1435 catalogUIAdapters.remove(eventCatalogUIAdapter);
1436 if (catalogUIAdapters.size() > 0) {
1437 MediaPackage mediaPackage = indexService.getEventMediapackage(event);
1438 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1439
1440 DublinCoreMetadataCollection fields = catalogUIAdapter.getFields(mediaPackage);
1441 if (fields != null) {
1442 ExternalMetadataUtils.removeCollectionList(fields);
1443 metadataList.add(catalogUIAdapter, fields);
1444 }
1445 }
1446 }
1447 DublinCoreMetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter,
1448 new EmptyResourceListQuery());
1449 ExternalMetadataUtils.changeSubjectToSubjects(collection);
1450 ExternalMetadataUtils.removeCollectionList(collection);
1451 metadataList.add(eventCatalogUIAdapter, collection);
1452 if (WorkflowInstance.WorkflowState.RUNNING.toString().equals(event.getWorkflowState())) {
1453 metadataList.setLocked(Locked.WORKFLOW_RUNNING);
1454 }
1455 return Optional.of(metadataList);
1456 }
1457
1458 private Optional<MediaPackageElementFlavor> getFlavor(String flavorString) {
1459 try {
1460 MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
1461 return Optional.of(flavor);
1462 } catch (IllegalArgumentException e) {
1463 return Optional.empty();
1464 }
1465 }
1466
1467 private Response getEventMetadataByType(String id, String type, ApiVersion requestedVersion) throws Exception {
1468 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1469 if (eventOpt.isPresent()) {
1470 Event event = eventOpt.get();
1471 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
1472 if (flavor.isEmpty()) {
1473 return R.badRequest(
1474 String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
1475 }
1476
1477 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1478 if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
1479 DublinCoreMetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter,
1480 new EmptyResourceListQuery());
1481 ExternalMetadataUtils.changeSubjectToSubjects(collection);
1482 ExternalMetadataUtils.removeCollectionList(collection);
1483 convertStartDateTimeToApiV1(collection);
1484 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.collectionToJson(collection, false));
1485 }
1486
1487 List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
1488 catalogUIAdapters.remove(eventCatalogUIAdapter);
1489 if (catalogUIAdapters.size() > 0) {
1490 MediaPackage mediaPackage = indexService.getEventMediapackage(event);
1491 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1492 if (flavor.get().equals(catalogUIAdapter.getFlavor())) {
1493 DublinCoreMetadataCollection fields = catalogUIAdapter.getFields(mediaPackage);
1494 ExternalMetadataUtils.removeCollectionList(fields);
1495 convertStartDateTimeToApiV1(fields);
1496 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.collectionToJson(fields, false));
1497 }
1498 }
1499 }
1500 return ApiResponseBuilder.notFound("Cannot find a catalog with type '%s' for event with id '%s'.", type, id);
1501 }
1502 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1503 }
1504
1505 @PUT
1506 @Path("{eventId}/metadata")
1507 @RestQuery(name = "updateeventmetadata", description = "Update the metadata with the matching type of the specified event. For a metadata catalog there is the flavor such as 'dublincore/episode' and this is the unique type.", returnDescription = "", pathParameters = {
1508 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
1509 @RestParameter(name = "type", isRequired = true, description = "The type of metadata to update", type = STRING),
1510 @RestParameter(name = "metadata", description = "Metadata catalog in JSON format", isRequired = true, type = STRING) }, responses = {
1511 @RestResponse(description = "The metadata of the given namespace has been updated.", responseCode = HttpServletResponse.SC_OK),
1512 @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1513 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1514 public Response updateEventMetadataByType(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1515 @QueryParam("type") String type, @FormParam("metadata") String metadataJSON) throws Exception {
1516 Map<String, String> updatedFields;
1517 JSONParser parser = new JSONParser();
1518 try {
1519 updatedFields = RequestUtils.getKeyValueMap(metadataJSON);
1520 } catch (ParseException e) {
1521 logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
1522 return RestUtil.R.badRequest(String.format("Unable to parse metadata fields as json from '%s'", metadataJSON));
1523 } catch (IllegalArgumentException e) {
1524 logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
1525 return RestUtil.R.badRequest(e.getMessage());
1526 }
1527
1528 if (updatedFields == null || updatedFields.size() == 0) {
1529 return RestUtil.R.badRequest(
1530 String.format("Unable to parse metadata fields as json from '%s' because there were no fields to update.",
1531 metadataJSON));
1532 }
1533
1534 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
1535 if (flavor.isEmpty()) {
1536 return R.badRequest(
1537 String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
1538 }
1539
1540 DublinCoreMetadataCollection collection = null;
1541 EventCatalogUIAdapter adapter = null;
1542 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1543 if (eventOpt.isPresent()) {
1544 Event event = eventOpt.get();
1545 MetadataList metadataList = new MetadataList();
1546
1547 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1548 if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
1549 collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter);
1550 adapter = eventCatalogUIAdapter;
1551 } else {
1552 metadataList.add(eventCatalogUIAdapter, EventUtils.getEventMetadata(event, eventCatalogUIAdapter));
1553 }
1554
1555
1556 List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
1557 catalogUIAdapters.remove(eventCatalogUIAdapter);
1558 if (catalogUIAdapters.size() > 0) {
1559 MediaPackage mediaPackage = indexService.getEventMediapackage(event);
1560 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1561 if (flavor.get().equals(catalogUIAdapter.getFlavor())) {
1562 collection = catalogUIAdapter.getFields(mediaPackage);
1563 adapter = eventCatalogUIAdapter;
1564 } else {
1565 metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(mediaPackage));
1566 }
1567 }
1568 }
1569
1570 if (collection == null) {
1571 return ApiResponseBuilder.notFound("Cannot find a catalog with type '%s' for event with id '%s'.", type, id);
1572 }
1573
1574 for (String key : updatedFields.keySet()) {
1575 if ("subjects".equals(key)) {
1576 MetadataField field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
1577 Optional<Response> error = validateField(field, key, id, type, updatedFields);
1578 if (error.isPresent()) {
1579 return error.get();
1580 }
1581 collection.removeField(field);
1582 JSONArray subjectArray = (JSONArray) parser.parse(updatedFields.get(key));
1583 collection.addField(
1584 MetadataJson.copyWithDifferentJsonValue(field, StringUtils.join(subjectArray.iterator(), ",")));
1585 } else if ("startDate".equals(key)) {
1586
1587 MetadataField field = collection.getOutputFields().get(key);
1588 Optional<Response> error = validateField(field, key, id, type, updatedFields);
1589 if (error.isPresent()) {
1590 return error.get();
1591 }
1592 String apiPattern = field.getPattern();
1593 if (configuredMetadataFields.containsKey("startDate")) {
1594 final String startDate = configuredMetadataFields.get("startDate").getPattern();
1595 apiPattern = startDate == null ? apiPattern : startDate;
1596 }
1597 SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(apiPattern);
1598 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
1599 DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
1600 DateTime newStartDate = new DateTime(apiSdf.parse(updatedFields.get(key)), DateTimeZone.UTC);
1601 DateTime updatedStartDate = oldStartDate.withDate(newStartDate.year().get(), newStartDate.monthOfYear().get(), newStartDate.dayOfMonth().get());
1602 collection.removeField(field);
1603 collection.addField(
1604 MetadataJson.copyWithDifferentJsonValue(field, sdf.format(updatedStartDate.toDate())));
1605 } else if ("startTime".equals(key)) {
1606
1607 MetadataField field = collection.getOutputFields().get("startDate");
1608 Optional<Response> error = validateField(field, "startDate", id, type, updatedFields);
1609 if (error.isPresent()) {
1610 return error.get();
1611 }
1612 String apiPattern = "HH:mm";
1613 if (configuredMetadataFields.containsKey("startTime")) {
1614 final String startTime = configuredMetadataFields.get("startTime").getPattern();
1615 apiPattern = startTime == null ? apiPattern : startTime;
1616 }
1617 SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(apiPattern);
1618 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
1619 DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
1620 DateTime newStartDate = new DateTime(apiSdf.parse(updatedFields.get(key)), DateTimeZone.UTC);
1621 DateTime updatedStartDate = oldStartDate.withTime(
1622 newStartDate.hourOfDay().get(),
1623 newStartDate.minuteOfHour().get(),
1624 newStartDate.secondOfMinute().get(),
1625 newStartDate.millisOfSecond().get());
1626 collection.removeField(field);
1627 collection.addField(
1628 MetadataJson.copyWithDifferentJsonValue(field, sdf.format(updatedStartDate.toDate())));
1629 } else {
1630 MetadataField field = collection.getOutputFields().get(key);
1631 Optional<Response> error = validateField(field, key, id, type, updatedFields);
1632 if (error.isPresent()) {
1633 return error.get();
1634 }
1635 collection.removeField(field);
1636 collection.addField(
1637 MetadataJson.copyWithDifferentJsonValue(field, updatedFields.get(key)));
1638 }
1639 }
1640
1641 metadataList.add(adapter, collection);
1642 indexService.updateEventMetadata(id, metadataList, elasticsearchIndex);
1643 return Response.noContent().build();
1644 }
1645 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1646 }
1647
1648 private Optional<Response> validateField(MetadataField field, String key, String id, String type, Map<String, String> updatedFields) {
1649 if (field == null) {
1650 return Optional.of(ApiResponseBuilder.notFound(
1651 "Cannot find a metadata field with id '%s' from event with id '%s' and the metadata type '%s'.",
1652 key, id, type));
1653 } else if (field.isRequired() && StringUtils.isBlank(updatedFields.get(key))) {
1654 return Optional.of(R.badRequest(String.format(
1655 "The event metadata field with id '%s' and the metadata type '%s' is required and can not be empty!.",
1656 key, type)));
1657 }
1658 return Optional.empty();
1659 }
1660
1661 @DELETE
1662 @Path("{eventId}/metadata")
1663 @RestQuery(name = "deleteeventmetadata", description = "Delete the metadata namespace catalog of the specified event. This will remove all fields and values of the catalog.", returnDescription = "", pathParameters = {
1664 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
1665 @RestParameter(name = "type", isRequired = true, description = "The type of metadata to delete", type = STRING) }, responses = {
1666 @RestResponse(description = "The metadata of the given namespace has been updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1667 @RestResponse(description = "The main metadata catalog dublincore/episode cannot be deleted as it has mandatory fields.", responseCode = HttpServletResponse.SC_FORBIDDEN),
1668 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1669 public Response deleteEventMetadataByType(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1670 @QueryParam("type") String type) throws SearchIndexException {
1671 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1672 if (eventOpt.isPresent()) {
1673 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
1674 if (flavor.isEmpty()) {
1675 return R.badRequest(
1676 String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
1677 }
1678 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1679 if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
1680 return Response
1681 .status(Status.FORBIDDEN).entity(String
1682 .format("Unable to delete mandatory metadata catalog with type '%s' for event '%s'", type, id))
1683 .build();
1684 }
1685 try {
1686 indexService.removeCatalogByFlavor(eventOpt.get(), flavor.get());
1687 } catch (NotFoundException e) {
1688 return ApiResponseBuilder.notFound(e.getMessage());
1689 } catch (IndexServiceException e) {
1690 logger.error("Unable to remove metadata catalog with type '{}' from event '{}'", type, id, e);
1691 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1692 } catch (IllegalStateException e) {
1693 logger.debug("Unable to remove metadata catalog with type '{}' from event '{}'", type, id, e);
1694 throw new WebApplicationException(e, Status.BAD_REQUEST);
1695 } catch (UnauthorizedException e) {
1696 return Response.status(Status.UNAUTHORIZED).build();
1697 }
1698 return Response.noContent().build();
1699 }
1700 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1701 }
1702
1703 @GET
1704 @Path("{eventId}/publications")
1705 @RestQuery(name = "geteventpublications", description = "Returns an event's list of publications.",
1706 returnDescription = "",
1707 pathParameters = {
1708 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1709 },
1710 restParameters = {
1711 @RestParameter(name = "sign", description = "Whether public distribution urls should be signed.",
1712 isRequired = false, type = Type.BOOLEAN),
1713 @RestParameter(name = "includeInternalPublication", description = "Whether internal publications should be included.",
1714 isRequired = false, type = Type.BOOLEAN)
1715 },
1716 responses = {
1717 @RestResponse(description = "The list of publications is returned.", responseCode = HttpServletResponse.SC_OK),
1718 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1719
1720 public Response getEventPublications(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1721 @QueryParam("sign") boolean sign, @QueryParam("includeInternalPublication") boolean includeInternalPublication) throws Exception {
1722 try {
1723 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
1724 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
1725 if (event.isPresent()) {
1726 JsonArray jsonArray = new JsonArray();
1727 for (JsonElement pub : getPublications(event.get(), sign, includeInternalPublication, requestedVersion)) {
1728 jsonArray.add(pub);
1729 }
1730 return ApiResponseBuilder.Json.ok(acceptHeader, jsonArray);
1731 } else {
1732 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
1733 }
1734 } catch (SearchIndexException e) {
1735 logger.error("Unable to get list of publications from event with id '{}'", id, e);
1736 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1737 }
1738 }
1739
1740 private List<JsonObject> getPublications(Event event, Boolean withSignedUrls, Boolean includeInternalPublication, ApiVersion requestedVersion) {
1741 return event.getPublications().stream()
1742 .filter(publication -> {
1743 boolean isInternalAllowed = includeInternalPublication != null && includeInternalPublication && !requestedVersion.isSmallerThan(VERSION_1_11_0);
1744 return isInternalAllowed || EventUtils.internalChannelFilter.test(publication);
1745 })
1746 .map(p -> getPublication(p, withSignedUrls, requestedVersion))
1747 .collect(Collectors.toList());
1748 }
1749
1750 public JsonObject getPublication(Publication publication, Boolean sign, ApiVersion requestedVersion) {
1751
1752 URI publicationUrl = publication.getURI();
1753 if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
1754 publicationUrl = getSignedUrl(publicationUrl, sign);
1755 }
1756
1757 JsonObject json = new JsonObject();
1758 json.addProperty("id", publication.getIdentifier());
1759 json.addProperty("channel", publication.getChannel());
1760 json.addProperty("mediatype", safeString(publication.getMimeType()));
1761 json.addProperty("url", publicationUrl != null ? publicationUrl.toString() : "");
1762 JsonArray mediaArray = new JsonArray();
1763 for (JsonObject trackJson : getPublicationTracksJson(publication, sign, requestedVersion)) {
1764 mediaArray.add(trackJson);
1765 }
1766 json.add("media", mediaArray);
1767 JsonArray attachmentArray = new JsonArray();
1768 for (JsonObject attachmentJson : getPublicationAttachmentsJson(publication, sign)) {
1769 attachmentArray.add(attachmentJson);
1770 }
1771 json.add("attachments", attachmentArray);
1772 JsonArray metadataArray = new JsonArray();
1773 for (JsonObject catalogJson : getPublicationCatalogsJson(publication, sign)) {
1774 metadataArray.add(catalogJson);
1775 }
1776 json.add("metadata", metadataArray);
1777
1778 return json;
1779 }
1780
1781 private URI getSignedUrl(URI url, boolean sign) {
1782 if (url == null || !sign) {
1783 return url;
1784 }
1785
1786 if (urlSigningService.accepts(url.toString())) {
1787 try {
1788 return URI.create(urlSigningService.sign(url.toString(), expireSeconds, null, null));
1789 } catch (UrlSigningException e) {
1790 logger.error("Unable to sign URI {}", url, e);
1791 }
1792 }
1793 return url;
1794 }
1795
1796 private List<JsonObject> getPublicationTracksJson(Publication publication, Boolean sign, ApiVersion requestedVersion) {
1797 List<JsonObject> tracksJson = new ArrayList<>();
1798
1799 for (Track track : publication.getTracks()) {
1800 JsonObject trackJson = new JsonObject();
1801
1802 trackJson.addProperty("id", safeString(track.getIdentifier()));
1803 trackJson.addProperty("mediatype", safeString(track.getMimeType()));
1804 trackJson.addProperty("url", safeString(getSignedUrl(track.getURI(), sign)));
1805 trackJson.addProperty("flavor", safeString(track.getFlavor()));
1806 trackJson.addProperty("size", track.getSize());
1807 trackJson.addProperty("checksum", safeString(track.getChecksum()));
1808 trackJson.add("tags", arrayToJsonArray(track.getTags()));
1809 trackJson.addProperty("has_audio", track.hasAudio());
1810 trackJson.addProperty("has_video", track.hasVideo());
1811 trackJson.addProperty("duration", track.getDuration() != null ? track.getDuration() : null);
1812 trackJson.addProperty("description", safeString(track.getDescription()));
1813
1814 VideoStream[] videoStreams = TrackSupport.byType(track.getStreams(), VideoStream.class);
1815 if (videoStreams.length > 0) {
1816
1817 VideoStream videoStream = videoStreams[0];
1818 if (videoStream.getBitRate() != null)
1819 trackJson.addProperty("bitrate", videoStream.getBitRate());
1820 if (videoStream.getFrameRate() != null)
1821 trackJson.addProperty("framerate", videoStream.getFrameRate());
1822 if (videoStream.getFrameCount() != null)
1823 trackJson.addProperty("framecount", videoStream.getFrameCount());
1824 if (videoStream.getFrameWidth() != null)
1825 trackJson.addProperty("width", videoStream.getFrameWidth());
1826 if (videoStream.getFrameHeight() != null)
1827 trackJson.addProperty("height", videoStream.getFrameHeight());
1828 }
1829
1830 if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
1831 trackJson.addProperty("is_master_playlist", track.isMaster());
1832 trackJson.addProperty("is_live", track.isLive());
1833 }
1834
1835 tracksJson.add(trackJson);
1836 }
1837
1838 return tracksJson;
1839 }
1840
1841 private List<JsonObject> getPublicationAttachmentsJson(Publication publication, Boolean sign) {
1842 List<JsonObject> attachmentsJson = new ArrayList<>();
1843
1844 for (Attachment attachment : publication.getAttachments()) {
1845 JsonObject json = new JsonObject();
1846
1847 json.addProperty("id", safeString(attachment.getIdentifier()));
1848 json.addProperty("mediatype", safeString(attachment.getMimeType()));
1849 json.addProperty("url", safeString(getSignedUrl(attachment.getURI(), sign)));
1850 json.addProperty("flavor", safeString(attachment.getFlavor()));
1851 json.addProperty("ref", safeString(attachment.getReference()));
1852 json.addProperty("size", attachment.getSize());
1853 json.addProperty("checksum", safeString(attachment.getChecksum()));
1854 json.add("tags", arrayToJsonArray(attachment.getTags()));
1855
1856 attachmentsJson.add(json);
1857 }
1858
1859 return attachmentsJson;
1860 }
1861
1862 private List<JsonObject> getPublicationCatalogsJson(Publication publication, Boolean sign) {
1863 List<JsonObject> catalogsJson = new ArrayList<>();
1864
1865 for (Catalog catalog : publication.getCatalogs()) {
1866 JsonObject json = new JsonObject();
1867
1868 json.addProperty("id", safeString(catalog.getIdentifier()));
1869 json.addProperty("mediatype", safeString(catalog.getMimeType()));
1870 json.addProperty("url", safeString(getSignedUrl(catalog.getURI(), sign)));
1871 json.addProperty("flavor", safeString(catalog.getFlavor()));
1872 json.addProperty("size", catalog.getSize());
1873 json.addProperty("checksum", safeString(catalog.getChecksum()));
1874 json.add("tags", arrayToJsonArray(catalog.getTags()));
1875
1876 catalogsJson.add(json);
1877 }
1878
1879 return catalogsJson;
1880 }
1881
1882 @GET
1883 @Path("{eventId}/publications/{publicationId}")
1884 @RestQuery(name = "geteventpublication", description = "Returns a single publication.", returnDescription = "",
1885 pathParameters = {
1886 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
1887 @RestParameter(name = "publicationId", description = "The publication id", isRequired = true, type = STRING)
1888 },
1889 restParameters = {
1890 @RestParameter(name = "sign", description = "Whether public distribution urls should be signed.",
1891 isRequired = false, type = Type.BOOLEAN)
1892 },
1893 responses = {
1894 @RestResponse(description = "The track details are returned.", responseCode = HttpServletResponse.SC_OK),
1895 @RestResponse(description = "The specified event or publication does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1896
1897 public Response getEventPublication(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String eventId,
1898 @PathParam("publicationId") String publicationId, @QueryParam("sign") boolean sign) throws Exception {
1899 try {
1900 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
1901 return ApiResponseBuilder.Json.ok(acceptHeader, getPublication(eventId, publicationId, sign, requestedVersion));
1902 } catch (NotFoundException e) {
1903 return ApiResponseBuilder.notFound(e.getMessage());
1904 } catch (SearchIndexException e) {
1905 logger.error("Unable to get list of publications from event with id '{}'", eventId, e);
1906 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1907 }
1908 }
1909
1910
1911 private JsonObject getPublication(String eventId, String publicationId, Boolean withSignedUrls, ApiVersion requestedVersion)
1912 throws SearchIndexException, NotFoundException {
1913 Optional<Event> eventOpt = indexService.getEvent(eventId, elasticsearchIndex);
1914 if (eventOpt.isPresent()) {
1915 List<Publication> publications;
1916 publications = eventOpt.get().getPublications().stream().filter(publication -> (!requestedVersion.isSmallerThan(VERSION_1_11_0) || EventUtils.internalChannelFilter.test(publication))).collect(Collectors.toList());
1917 for (Publication publication : publications) {
1918 if (publicationId.equals(publication.getIdentifier())) {
1919 return getPublication(publication, withSignedUrls, requestedVersion);
1920 }
1921 }
1922 throw new NotFoundException(
1923 String.format("Unable to find publication with id '%s' in event with id '%s'", publicationId, eventId));
1924 }
1925 throw new NotFoundException(String.format("Unable to find event with id '%s'", eventId));
1926 }
1927
1928
1929
1930
1931
1932
1933
1934
1935 protected static AccessControlList getAclFromEvent(Event event) {
1936 AccessControlList activeAcl = new AccessControlList();
1937 try {
1938 if (event.getAccessPolicy() != null)
1939 activeAcl = AccessControlParser.parseAcl(event.getAccessPolicy());
1940 } catch (Exception e) {
1941 logger.error("Unable to parse access policy", e);
1942 }
1943 return activeAcl;
1944 }
1945
1946 private JsonObject getJsonStream(Stream stream) {
1947 JsonObject json = new JsonObject();
1948
1949 if (stream instanceof AudioStream) {
1950 AudioStream audio = (AudioStream) stream;
1951
1952 if (audio.getBitDepth() != null) json.addProperty("bitdepth", audio.getBitDepth());
1953 if (audio.getBitRate() != null) json.addProperty("bitrate", audio.getBitRate());
1954 if (audio.getCaptureDevice() != null) json.addProperty("capturedevice", audio.getCaptureDevice());
1955 if (audio.getCaptureDeviceVendor() != null) json.addProperty("capturedevicevendor", audio.getCaptureDeviceVendor());
1956 if (audio.getCaptureDeviceVersion() != null) json.addProperty("capturedeviceversion", audio.getCaptureDeviceVersion());
1957 if (audio.getChannels() != null) json.addProperty("channels", audio.getChannels());
1958 if (audio.getEncoderLibraryVendor() != null) json.addProperty("encoderlibraryvendor", audio.getEncoderLibraryVendor());
1959 if (audio.getFormat() != null) json.addProperty("format", audio.getFormat());
1960 if (audio.getFormatVersion() != null) json.addProperty("formatversion", audio.getFormatVersion());
1961 if (audio.getFrameCount() != null) json.addProperty("framecount", audio.getFrameCount());
1962 if (audio.getIdentifier() != null) json.addProperty("identifier", audio.getIdentifier());
1963 if (audio.getPkLevDb() != null) json.addProperty("pklevdb", audio.getPkLevDb());
1964 if (audio.getRmsLevDb() != null) json.addProperty("rmslevdb", audio.getRmsLevDb());
1965 if (audio.getRmsPkDb() != null) json.addProperty("rmspkdb", audio.getRmsPkDb());
1966 if (audio.getSamplingRate() != null) json.addProperty("samplingrate", audio.getSamplingRate());
1967
1968 } else if (stream instanceof VideoStream) {
1969 VideoStream video = (VideoStream) stream;
1970
1971 if (video.getBitRate() != null) json.addProperty("bitrate", video.getBitRate());
1972 if (video.getCaptureDevice() != null) json.addProperty("capturedevice", video.getCaptureDevice());
1973 if (video.getCaptureDeviceVendor() != null) json.addProperty("capturedevicevendor", video.getCaptureDeviceVendor());
1974 if (video.getCaptureDeviceVersion() != null) json.addProperty("capturedeviceversion", video.getCaptureDeviceVersion());
1975 if (video.getEncoderLibraryVendor() != null) json.addProperty("encoderlibraryvendor", video.getEncoderLibraryVendor());
1976 if (video.getFormat() != null) json.addProperty("format", video.getFormat());
1977 if (video.getFormatVersion() != null) json.addProperty("formatversion", video.getFormatVersion());
1978 if (video.getFrameCount() != null) json.addProperty("framecount", video.getFrameCount());
1979 if (video.getFrameHeight() != null) json.addProperty("frameheight", video.getFrameHeight());
1980 if (video.getFrameRate() != null) json.addProperty("framerate", video.getFrameRate());
1981 if (video.getFrameWidth() != null) json.addProperty("framewidth", video.getFrameWidth());
1982 if (video.getIdentifier() != null) json.addProperty("identifier", video.getIdentifier());
1983 if (video.getScanOrder() != null) json.addProperty("scanorder", video.getScanOrder().toString());
1984 if (video.getScanType() != null) json.addProperty("scantype", video.getScanType().toString());
1985 }
1986
1987 return json;
1988 }
1989
1990 private String getEventUrl(String eventId) {
1991 return UrlSupport.concat(endpointBaseUrl, eventId);
1992 }
1993
1994 @GET
1995 @Path("{eventId}/scheduling")
1996 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
1997 ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
1998 ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
1999 ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
2000 @RestQuery(name = "geteventscheduling", description = "Returns an event's scheduling information.", returnDescription = "", pathParameters = {
2001 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
2002 @RestResponse(description = "The scheduling information for the specified event is returned.", responseCode = HttpServletResponse.SC_OK),
2003 @RestResponse(description = "The specified event has no scheduling information.", responseCode = HttpServletResponse.SC_NO_CONTENT),
2004 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2005 public Response getEventScheduling(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
2006 throws Exception {
2007 try {
2008 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
2009
2010 if (event.isEmpty()) {
2011 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
2012 }
2013
2014 final JsonObject scheduling = SchedulingInfo.of(event.get().getIdentifier(), schedulerService).toJson();
2015 if (!scheduling.isEmpty()) {
2016 return ApiResponseBuilder.Json.ok(acceptHeader, scheduling);
2017 }
2018 return Response.noContent().build();
2019 } catch (SearchIndexException e) {
2020 logger.error("Unable to get list of publications from event with id '{}'", id, e);
2021 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
2022 }
2023 }
2024
2025 @PUT
2026 @Path("{eventId}/scheduling")
2027 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
2028 ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
2029 ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
2030 ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
2031 @RestQuery(name = "updateeventscheduling", description = "Update an event's scheduling information.", returnDescription = "", pathParameters = {
2032 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = Type.STRING) }, restParameters = {
2033 @RestParameter(name = "scheduling", isRequired = true, description = "Scheduling Information", type = Type.STRING),
2034 @RestParameter(name = "allowConflict", description = "Allow conflicts when updating scheduling", isRequired = false, type = Type.BOOLEAN) }, responses = {
2035 @RestResponse(description = "The scheduling information for the specified event is updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
2036 @RestResponse(description = "The specified event has no scheduling information to update.", responseCode = HttpServletResponse.SC_NOT_ACCEPTABLE),
2037 @RestResponse(description = "The scheduling information could not be updated due to a conflict.", responseCode = HttpServletResponse.SC_CONFLICT),
2038 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2039 public Response updateEventScheduling(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
2040 @FormParam("scheduling") String scheduling,
2041 @FormParam("allowConflict") @DefaultValue("false") boolean allowConflict) throws Exception {
2042 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
2043 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
2044
2045 if (requestedVersion.isSmallerThan(ApiVersion.VERSION_1_2_0)) {
2046 allowConflict = false;
2047 }
2048 if (event.isEmpty()) {
2049 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
2050 }
2051 final JSONParser parser = new JSONParser();
2052 JSONObject parsedJson;
2053 try {
2054 parsedJson = (JSONObject) parser.parse(scheduling);
2055 } catch (ParseException e) {
2056 logger.debug("Client sent unparsable scheduling information for event {}: {}", id, scheduling);
2057 return RestUtil.R.badRequest("Unparsable scheduling information");
2058 }
2059 Optional<Response> clientError = updateSchedulingInformation(parsedJson, id, requestedVersion, allowConflict);
2060 return clientError.orElse(Response.noContent().build());
2061 }
2062
2063 private Optional<Response> updateSchedulingInformation(
2064 JSONObject parsedScheduling,
2065 String id,
2066 ApiVersion requestedVersion,
2067 boolean allowConflict) throws Exception {
2068
2069 SchedulingInfo schedulingInfo;
2070 try {
2071 schedulingInfo = SchedulingInfo.of(parsedScheduling);
2072 } catch (DateTimeParseException e) {
2073 logger.debug("Client sent unparsable start or end date for event {}", id);
2074 return Optional.of(RestUtil.R.badRequest("Unparsable date in scheduling information"));
2075 }
2076 final TechnicalMetadata technicalMetadata = schedulerService.getTechnicalMetadata(id);
2077
2078
2079 Optional<Map<String, String>> caConfig = Optional.empty();
2080 if (schedulingInfo.getInputs().isPresent()) {
2081 final Map<String, String> configMap = new HashMap<>(technicalMetadata.getCaptureAgentConfiguration());
2082 configMap.put(CaptureParameters.CAPTURE_DEVICE_NAMES, schedulingInfo.getInputs().get());
2083 caConfig = Optional.of(configMap);
2084 }
2085
2086 try {
2087 schedulerService.updateEvent(
2088 id,
2089 schedulingInfo.getStartDate(),
2090 schedulingInfo.getEndDate(),
2091 schedulingInfo.getAgentId(),
2092 Optional.empty(),
2093 Optional.empty(),
2094 Optional.empty(),
2095 caConfig,
2096 allowConflict);
2097 } catch (SchedulerConflictException e) {
2098 final List<MediaPackage> conflictingEvents = getConflictingEvents(
2099 schedulingInfo.merge(technicalMetadata), agentStateService, schedulerService);
2100 logger.debug("Client tried to change scheduling information causing a conflict for event {}.", id);
2101 List<JsonObject> conflicts = convertConflictingEvents(
2102 Optional.of(id), conflictingEvents, indexService, elasticsearchIndex
2103 );
2104
2105 JsonArray conflictArray = new JsonArray();
2106 for (JsonObject conflict : conflicts) {
2107 conflictArray.add(conflict);
2108 }
2109
2110 return Optional.of(ApiResponseBuilder.Json.conflict(requestedVersion, conflictArray));
2111 }
2112 return Optional.empty();
2113 }
2114
2115 @POST
2116 @Path("{eventId}/track")
2117 @Consumes(MediaType.MULTIPART_FORM_DATA)
2118 @RestQuery(name = "updateFlavorWithTrack", description = "Update an events track for a given flavor", returnDescription = "",
2119 pathParameters = {
2120 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) },
2121 restParameters = {
2122 @RestParameter(description = "Flavor to add track to, e.g. captions/source",
2123 isRequired = true, name = "flavor", type = RestParameter.Type.STRING),
2124 @RestParameter(description = "Comma separated list of tags for the given track, e.g. archive,publish. "
2125 + "If a 'lang:LANG-CODE' tag exists and overwriteExisting=true "
2126 + "only tracks with same lang tag and flavor will be replaced. This behavior is used for captions.",
2127 isRequired = false, name = "tags", type = RestParameter.Type.STRING),
2128 @RestParameter(description = "If true, all other tracks in the specified flavor are REMOVED. "
2129 + "If tags argument contains a lang:LANG-CODE tag, only elements with same tag would be removed.",
2130 isRequired = true, name = "overwriteExisting", type = RestParameter.Type.BOOLEAN),
2131 @RestParameter(description = "The track file", isRequired = true, name = "track", type = RestParameter.Type.FILE),
2132 },
2133 responses = {
2134 @RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND),
2135 @RestResponse(description = "The track has been added to the event.", responseCode = HttpServletResponse.SC_OK),
2136 @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2137 })
2138 public Response updateFlavorWithTrack(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
2139 @Context HttpServletRequest request) {
2140 logger.debug("updateFlavorWithTrack called");
2141 try {
2142 boolean overwriteExisting = false;
2143 MediaPackageElementFlavor tmpFlavor = MediaPackageElementFlavor.parseFlavor("addTrack/temporary");
2144 MediaPackageElementFlavor newFlavor = null;
2145 Optional<Event> event;
2146 List<String> tags = null;
2147 String langTag = null;
2148
2149 try {
2150 event = indexService.getEvent(id, elasticsearchIndex);
2151 } catch (SearchIndexException e) {
2152 return RestUtil.R.badRequest(String.format("Error while searching for event with id %s; %s", id, e.getMessage()));
2153 }
2154
2155 if (event.isEmpty()) {
2156 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
2157 }
2158 MediaPackage mp = indexService.getEventMediapackage(event.get());
2159
2160 try {
2161 if (workflowService.mediaPackageHasActiveWorkflows(mp.getIdentifier().toString())) {
2162 return RestUtil.R.conflict(String.format("Cannot update while a workflow is running on event '%s'", id));
2163 }
2164 } catch (WorkflowDatabaseException e) {
2165 return RestUtil.R.serverError();
2166 }
2167
2168 if (!ServletFileUpload.isMultipartContent(request)) {
2169 throw new IllegalArgumentException("No multipart content");
2170 }
2171 for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
2172 FileItemStream item = iter.next();
2173 String fieldName = item.getFieldName();
2174 if (item.isFormField()) {
2175 if ("flavor".equals(fieldName)) {
2176 String flavorString = Streams.asString(item.openStream());
2177 try {
2178 newFlavor = MediaPackageElementFlavor.parseFlavor(flavorString);
2179 } catch (IllegalArgumentException e) {
2180 return RestUtil.R.badRequest(String.format("Could not parse flavor %s; %s", flavorString, e.getMessage()));
2181 }
2182 } else if ("tags".equals(fieldName)) {
2183 String tagsString = Streams.asString(item.openStream());
2184 if (StringUtils.isNotBlank(tagsString)) {
2185 tags = List.of(StringUtils.split(tagsString, ','));
2186
2187 for (String tag : tags) {
2188 if (StringUtils.startsWith(StringUtils.trimToEmpty(tag), "lang:")) {
2189
2190 langTag = StringUtils.trimToEmpty(tag);
2191 break;
2192 }
2193 }
2194 }
2195 } else if ("overwriteExisting".equals(fieldName)) {
2196 overwriteExisting = Boolean.parseBoolean(Streams.asString(item.openStream()));
2197 }
2198 } else {
2199
2200 if ("track".equals(item.getFieldName())) {
2201 mp = ingestService.addTrack(item.openStream(), item.getName(), tmpFlavor, mp);
2202 }
2203 }
2204 }
2205
2206 if (overwriteExisting) {
2207
2208 Track[] existing = mp.getTracks(newFlavor);
2209 for (int i = 0; i < existing.length; i++) {
2210
2211 if (null == langTag || existing[i].containsTag(langTag)) {
2212 mp.remove(existing[i]);
2213 logger.debug("Overwriting existing asset {} {}", tmpFlavor, newFlavor);
2214 }
2215 }
2216 }
2217
2218 for (Track track : mp.getTracks(tmpFlavor)) {
2219 track.setFlavor(newFlavor);
2220 if (null != tags) {
2221 for (String tag : tags) {
2222 track.addTag(tag);
2223 }
2224 }
2225 }
2226 logger.debug("Updated asset {} {}", tmpFlavor, newFlavor);
2227
2228 try {
2229 assetManager.takeSnapshot(mp);
2230 } catch (AssetManagerException e) {
2231 logger.error("Error while adding the updated media package ({}) to the archive", mp.getIdentifier(), e);
2232 return RestUtil.R.badRequest(e.getMessage());
2233 }
2234
2235 return Response.status(Status.OK).build();
2236 } catch (IllegalArgumentException | IOException | FileUploadException | IndexServiceException | IngestException
2237 | MediaPackageException e) {
2238 return RestUtil.R.badRequest(String.format("Could not add track: %s", e.getMessage()));
2239 }
2240 }
2241 }