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(
416 name = "getevent",
417 description = "Returns a single event. By setting the optional sign parameter to true, the method will pre-sign "
418 + "distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of "
419 + "signed URLs when caching this response.",
420 returnDescription = "",
421 pathParameters = {
422 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
423 },
424 restParameters = {
425 @RestParameter(name = "sign", isRequired = false, description = "Whether public distribution urls should be "
426 + "signed.", type = Type.BOOLEAN),
427 @RestParameter(name = "withacl", isRequired = false, description = "Whether the acl metadata should be "
428 + "included in the response.", type = Type.BOOLEAN),
429 @RestParameter(name = "withmetadata", isRequired = false, description = "Whether the metadata catalogs "
430 + "should be included in the response.", type = Type.BOOLEAN),
431 @RestParameter(name = "withscheduling", isRequired = false, description = "Whether the scheduling "
432 + "information should be included in the response.", type = Type.BOOLEAN),
433 @RestParameter(name = "withpublications", isRequired = false, description = "Whether the publication ids and "
434 + "urls should be included in the response.", type = Type.BOOLEAN),
435 @RestParameter(name = "includeInternalPublication", isRequired = false, description = "Whether internal "
436 + "publications should be included.", type = Type.BOOLEAN)
437 },
438 responses = {
439 @RestResponse(description = "The event is returned.", responseCode = HttpServletResponse.SC_OK),
440 @RestResponse(description = "The specified event does not exist.",
441 responseCode = HttpServletResponse.SC_NOT_FOUND)
442 })
443 @Operation(
444 summary = "Get a single event",
445 description = "Returns a single event. By setting the optional sign parameter to true, the method will pre-sign "
446 + "distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of "
447 + "signed URLs when caching this response."
448 )
449 public Response getEvent(
450 @HeaderParam("Accept") String acceptHeader,
451 @Parameter(description = "The event id", required = true)
452 @PathParam("eventId") String id,
453 @Parameter(description = "Whether public distribution urls should be signed.")
454 @QueryParam("sign") boolean sign,
455 @Parameter(description = "Whether the acl metadata should be included in the response.")
456 @QueryParam("withacl") Boolean withAcl,
457 @Parameter(description = "Whether the metadata catalogs should be included in the response.")
458 @QueryParam("withmetadata") Boolean withMetadata,
459 @Parameter(description = "Whether the scheduling information should be included in the response.")
460 @QueryParam("withscheduling") Boolean withScheduling,
461 @Parameter(description = "Whether the publication ids and urls should be included in the response.")
462 @QueryParam("withpublications") Boolean withPublications,
463 @Parameter(description = "Whether internal publications should be included.")
464 @QueryParam("includeInternalPublication") Boolean includeInternalPublication)
465 throws Exception {
466 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
467 if (requestedVersion.isSmallerThan(VERSION_1_1_0)) {
468
469 withScheduling = false;
470 }
471 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
472 if (eventOpt.isPresent()) {
473 Event event = eventOpt.get();
474 event.updatePreview(previewSubtype);
475 return ApiResponseBuilder.Json.ok(
476 requestedVersion, eventToJSON(event, withAcl, withMetadata, withScheduling, withPublications,
477 includeInternalPublication, sign, requestedVersion));
478 }
479 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
480 }
481
482 @GET
483 @Path("{eventId}/media")
484 @RestQuery(
485 name = "geteventmedia",
486 description = "Returns media tracks of specific single event.",
487 returnDescription = "",
488 pathParameters = {
489 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
490 },
491 responses = {
492 @RestResponse(description = "The event's media is returned.", responseCode = HttpServletResponse.SC_OK),
493 @RestResponse(description = "The specified event does not exist.",
494 responseCode = HttpServletResponse.SC_NOT_FOUND)
495 })
496 @Operation(
497 summary = "Get media tracks of a single event",
498 description = "Returns media tracks of specific single event."
499 )
500 @Parameters({
501 @Parameter(name = "eventId", description = "The event id", required = true, in = ParameterIn.PATH),
502 @Parameter(name = "Accept", description = "The accept header", required = true, in = ParameterIn.HEADER)
503 })
504 @ApiResponses(value = {
505 @ApiResponse(responseCode = "200", description = "The event's media is returned."),
506 @ApiResponse(responseCode = "404", description = "The specified event does not exist.")
507 })
508 public Response getEventMedia(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
509 throws Exception {
510 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
511 List<TrackImpl> tracks = new ArrayList<>();
512
513 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
514 if (eventOpt.isPresent()) {
515 final MediaPackage mp = indexService.getEventMediapackage(eventOpt.get());
516 for (Track track : mp.getTracks()) {
517 if (track instanceof TrackImpl) {
518 tracks.add((TrackImpl) track);
519 }
520 }
521
522 JsonArray tracksJson = new JsonArray();
523 for (Track track : tracks) {
524 JsonObject trackJson = new JsonObject();
525 if (track.getChecksum() != null) {
526 trackJson.addProperty("checksum", track.getChecksum().toString());
527 }
528 if (track.getDescription() != null) {
529 trackJson.addProperty("description", track.getDescription());
530 }
531 if (track.getDuration() != null) {
532 trackJson.addProperty("duration", track.getDuration());
533 }
534 if (track.getElementDescription() != null) {
535 trackJson.addProperty("element-description", track.getElementDescription());
536 }
537 if (track.getFlavor() != null) {
538 trackJson.addProperty("flavor", track.getFlavor().toString());
539 }
540 if (track.getIdentifier() != null) {
541 trackJson.addProperty("identifier", track.getIdentifier());
542 }
543 if (track.getMimeType() != null) {
544 trackJson.addProperty("mimetype", track.getMimeType().toString());
545 }
546 trackJson.addProperty("size", track.getSize());
547
548 if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
549 trackJson.addProperty("has_video", track.hasVideo());
550 trackJson.addProperty("has_audio", track.hasAudio());
551 trackJson.addProperty("is_master_playlist", track.isMaster());
552 trackJson.addProperty("is_live", track.isLive());
553 }
554
555 if (track.getStreams() != null) {
556 JsonObject streamsJson = new JsonObject();
557 for (Stream stream : track.getStreams()) {
558 streamsJson.add(stream.getIdentifier(), getJsonStream(stream));
559 }
560 trackJson.add("streams", streamsJson);
561 }
562
563 trackJson.add("tags", arrayToJsonArray(track.getTags()));
564
565 if (track.getURI() != null) {
566 trackJson.addProperty("uri", track.getURI().toString());
567 }
568
569 tracksJson.add(trackJson);
570 }
571
572 return ApiResponseBuilder.Json.ok(acceptHeader, tracksJson);
573 }
574
575 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
576 }
577
578 @DELETE
579 @Path("{eventId}")
580 @RestQuery(
581 name = "deleteevent",
582 description = "Deletes an event.",
583 returnDescription = "",
584 pathParameters = {
585 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
586 },
587 responses = {
588 @RestResponse(description = "The event has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
589 @RestResponse(description = "The retraction of publications has started.",
590 responseCode = HttpServletResponse.SC_ACCEPTED),
591 @RestResponse(description = "The specified event does not exist.",
592 responseCode = HttpServletResponse.SC_NOT_FOUND)
593 })
594 public Response deleteEvent(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
595 throws SearchIndexException, UnauthorizedException {
596 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
597 if (event.isEmpty()) {
598 return RestUtil.R.notFound(id);
599 }
600 final IndexService.EventRemovalResult result;
601 try {
602 result = indexService.removeEvent(event.get(), retractWorkflowId);
603 } catch (WorkflowDatabaseException e) {
604 logger.error("Workflow database is not reachable. This may be a temporary problem.");
605 return RestUtil.R.serverError();
606 } catch (NotFoundException e) {
607 logger.error("Configured retract workflow not found. Check your configuration.");
608 return RestUtil.R.serverError();
609 }
610 switch (result) {
611 case SUCCESS:
612 return Response.noContent().build();
613 case RETRACTING:
614 return Response.accepted().build();
615 case GENERAL_FAILURE:
616 return Response.serverError().build();
617 case NOT_FOUND:
618 return RestUtil.R.notFound(id);
619 default:
620 throw new RuntimeException("Unknown EventRemovalResult type: " + result.name());
621 }
622 }
623
624 @POST
625 @Path("{eventId}")
626 @RestQuery(
627 name = "updateeventmetadata",
628 description = "Updates an event.",
629 returnDescription = "",
630 pathParameters = {
631 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
632 },
633 restParameters = {
634 @RestParameter(name = "acl", isRequired = false, description = "A collection of roles with their possible "
635 + "action", type = Type.STRING),
636 @RestParameter(name = "metadata", isRequired = false, description = "Event metadata as Form param",
637 type = Type.STRING),
638 @RestParameter(name = "scheduling", isRequired = false, description = "Scheduling information as Form param",
639 type = Type.STRING),
640 @RestParameter(name = "presenter", isRequired = false, description = "Presenter movie track",
641 type = Type.FILE),
642 @RestParameter(name = "presentation", isRequired = false, description = "Presentation movie track",
643 type = Type.FILE),
644 @RestParameter(name = "audio", isRequired = false, description = "Audio track", type = Type.FILE),
645 @RestParameter(name = "processing", isRequired = false, description = "Processing instructions task "
646 + "configuration", type = Type.STRING),
647 },
648 responses = {
649 @RestResponse(description = "The event has been updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
650 @RestResponse(description = "The event could not be updated due to a scheduling conflict.",
651 responseCode = HttpServletResponse.SC_CONFLICT),
652 @RestResponse(description = "The specified event does not exist.",
653 responseCode = HttpServletResponse.SC_NOT_FOUND)
654 })
655 public Response updateEventMetadata(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request,
656 @PathParam("eventId") String eventId) {
657 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
658 try {
659 String startDatePattern = configuredMetadataFields.containsKey("startDate")
660 ? configuredMetadataFields.get("startDate").getPattern() : null;
661 String startTimePattern = configuredMetadataFields.containsKey("startTime")
662 ? configuredMetadataFields.get("startTime").getPattern() : null;
663 Optional<Event> eventOpt = indexService.getEvent(eventId, elasticsearchIndex);
664 if (eventOpt.isPresent()) {
665 Event event = eventOpt.get();
666 EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.updateFromHttpServletRequest(event,
667 request, getEventCatalogUIAdapters(), startDatePattern, startTimePattern);
668
669
670 if (eventHttpServletRequest.getMetadataList().isPresent()) {
671 indexService.updateEventMetadata(eventId, eventHttpServletRequest.getMetadataList().get(),
672 elasticsearchIndex);
673 }
674
675 if (eventHttpServletRequest.getAcl().isPresent()) {
676 indexService.updateEventAcl(eventId, eventHttpServletRequest.getAcl().get(), elasticsearchIndex);
677 }
678
679 if (eventHttpServletRequest.getProcessing().isPresent()) {
680
681 if (!event.isScheduledEvent() || event.hasRecordingStarted()) {
682 return RestUtil.R.badRequest("Processing can't be updated for events that are already uploaded.");
683 }
684 JSONObject processing = eventHttpServletRequest.getProcessing().get();
685
686 String workflowId = (String) processing.get("workflow");
687 if (workflowId == null) {
688 throw new IllegalArgumentException("No workflow template in metadata");
689 }
690
691 Map<String, String> configuration = new HashMap<>();
692 if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
693 configuration = new HashMap<>(
694 (JSONObject) eventHttpServletRequest
695 .getProcessing().get()
696 .get("configuration"));
697 }
698
699 Optional<Map<String, String>> caMetadataOpt = Optional.empty();
700 Optional<Map<String, String>> workflowConfigOpt = Optional.empty();
701
702 Map<String, String> caMetadata = new HashMap<>(getSchedulerService().getCaptureAgentConfiguration(eventId));
703 if (!workflowId.equals(caMetadata.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION))) {
704 caMetadata.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowId);
705 caMetadataOpt = Optional.of(caMetadata);
706 }
707
708 Map<String, String> oldWorkflowConfig = new HashMap<>(getSchedulerService().getWorkflowConfig(eventId));
709 if (!oldWorkflowConfig.equals(configuration)) {
710 workflowConfigOpt = Optional.of(configuration);
711 }
712
713 if (!caMetadataOpt.isEmpty() || !workflowConfigOpt.isEmpty()) {
714 getSchedulerService().updateEvent(eventId, Optional.empty(), Optional.empty(), Optional.empty(),
715 Optional.empty(), Optional.empty(), workflowConfigOpt, caMetadataOpt);
716 }
717 }
718
719 if (eventHttpServletRequest.getScheduling().isPresent() && !requestedVersion.isSmallerThan(VERSION_1_1_0)) {
720
721 Optional<Response> clientError = updateSchedulingInformation(
722 eventHttpServletRequest.getScheduling().get(), eventId, requestedVersion, false);
723 if (clientError.isPresent()) {
724 return clientError.get();
725 }
726 }
727
728 return Response.noContent().build();
729 }
730 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", eventId);
731 } catch (NotFoundException e) {
732 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", eventId);
733 } catch (UnauthorizedException e) {
734 return Response.status(Status.UNAUTHORIZED).build();
735 } catch (IllegalArgumentException e) {
736 logger.debug("Unable to update event '{}'", eventId, e);
737 return RestUtil.R.badRequest(e.getMessage());
738 } catch (IndexServiceException e) {
739 logger.error("Unable to get multi part fields or file for event '{}'", eventId, e);
740 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
741 } catch (SearchIndexException e) {
742 logger.error("Unable to update event '{}'", eventId, e);
743 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
744 } catch (Exception e) {
745 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
746 }
747 }
748
749 @POST
750 @Path("/")
751 @Consumes(MediaType.MULTIPART_FORM_DATA)
752 @RestQuery(
753 name = "createevent",
754 description = "Creates an event by sending metadata, access control list, processing instructions and files in a "
755 + "multipart request.",
756 returnDescription = "",
757 restParameters = {
758 @RestParameter(name = "acl", isRequired = false, description = "A collection of roles with their possible "
759 + "action", type = STRING),
760 @RestParameter(name = "metadata", description = "Event metadata as Form param", isRequired = false,
761 type = STRING),
762 @RestParameter(name = "scheduling", description = "Scheduling information as Form param", isRequired = false,
763 type = STRING),
764 @RestParameter(name = "presenter", description = "Presenter movie track", isRequired = false,
765 type = Type.FILE),
766 @RestParameter(name = "presentation", description = "Presentation movie track", isRequired = false,
767 type = Type.FILE),
768 @RestParameter(name = "audio", description = "Audio track", isRequired = false, type = Type.FILE),
769 @RestParameter(name = "processing", description = "Processing instructions task configuration",
770 isRequired = false, type = STRING)
771 },
772 responses = {
773 @RestResponse(description = "A new event is created and its identifier is returned in the Location header.",
774 responseCode = HttpServletResponse.SC_CREATED),
775 @RestResponse(description = "The event could not be created due to a scheduling conflict.",
776 responseCode = HttpServletResponse.SC_CONFLICT),
777 @RestResponse(description = "The request is invalid or inconsistent..",
778 responseCode = HttpServletResponse.SC_BAD_REQUEST)
779 })
780 public Response createNewEvent(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request) {
781 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
782 try {
783 String startDatePattern = configuredMetadataFields.containsKey("startDate")
784 ? configuredMetadataFields.get("startDate").getPattern() : null;
785 String startTimePattern = configuredMetadataFields.containsKey("startTime")
786 ? configuredMetadataFields.get("startTime").getPattern() : null;
787 EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.createFromHttpServletRequest(request,
788 ingestService, getEventCatalogUIAdapters(), startDatePattern, startTimePattern);
789
790
791 if (eventHttpServletRequest.getScheduling().isPresent() && !requestedVersion.isSmallerThan(VERSION_1_1_0)) {
792
793 return scheduleNewEvent(eventHttpServletRequest, eventHttpServletRequest.getScheduling().get(),
794 requestedVersion);
795 }
796
797 JSONObject source = new JSONObject();
798 source.put("type", "UPLOAD");
799 eventHttpServletRequest.setSource(source);
800 String eventId = indexService.createEvent(eventHttpServletRequest);
801 JsonObject json = new JsonObject();
802 json.addProperty("identifier", eventId);
803 return ApiResponseBuilder.Json.created(requestedVersion, URI.create(getEventUrl(eventId)), json);
804 } catch (IllegalArgumentException | DateTimeParseException e) {
805 logger.debug("Unable to create event", e);
806 return RestUtil.R.badRequest(e.getMessage());
807 } catch (SchedulerException | IndexServiceException e) {
808 if (e.getCause() != null && e.getCause() instanceof NotFoundException
809 || e.getCause() instanceof IllegalArgumentException) {
810 logger.debug("Unable to create event", e);
811 return RestUtil.R.badRequest(e.getCause().getMessage());
812 } else {
813 logger.error("Unable to create event", e);
814 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
815 }
816 } catch (Exception e) {
817 logger.error("Unable to create event", e);
818 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
819 }
820 }
821
822 private Response scheduleNewEvent(EventHttpServletRequest request, JSONObject scheduling, ApiVersion requestedVersion)
823 throws MediaPackageException, IOException, IngestException, SchedulerException,
824 NotFoundException, UnauthorizedException, SearchIndexException, java.text.ParseException {
825
826 final SchedulingInfo schedulingInfo = SchedulingInfo.of(scheduling);
827 final JSONObject source = schedulingInfo.toSource();
828 request.setSource(source);
829
830 try {
831 final String eventId = indexService.createEvent(request);
832
833 if (StringUtils.isEmpty(eventId)) {
834 return RestUtil.R.badRequest("The date range provided did not include any events");
835 }
836
837 if (eventId.contains(",")) {
838
839 JsonArray eventArray = new JsonArray();
840 for (String id : eventId.split(",")) {
841 JsonObject eventObj = new JsonObject();
842 eventObj.addProperty("identifier", id);
843 eventArray.add(eventObj);
844 }
845 return ApiResponseBuilder.Json.ok(requestedVersion, eventArray);
846 }
847
848 JsonObject eventJson = new JsonObject();
849 eventJson.addProperty("identifier", eventId);
850 return ApiResponseBuilder.Json.created(requestedVersion, URI.create(getEventUrl(eventId)), eventJson);
851 } catch (SchedulerConflictException e) {
852 List<MediaPackage> conflictingEvents =
853 getConflictingEvents(schedulingInfo, agentStateService, schedulerService);
854 logger.debug("Client tried to schedule conflicting event(s).");
855 JsonArray conflictArray = new JsonArray();
856 for (JsonObject conflict : convertConflictingEvents(
857 Optional.empty(), conflictingEvents, indexService, elasticsearchIndex)) {
858 conflictArray.add(conflict);
859 }
860 return ApiResponseBuilder.Json.conflict(requestedVersion, conflictArray);
861 }
862 }
863
864 @GET
865 @Path("/")
866 @RestQuery(
867 name = "getevents",
868 description = "Returns a list of events. By setting the optional sign parameter to true, the method will "
869 + "pre-sign distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity "
870 + "of signed URLs when caching this response.",
871 returnDescription = "",
872 restParameters = {
873 @RestParameter(name = "sign", isRequired = false, description = "Whether public distribution urls should be "
874 + "signed.", type = Type.BOOLEAN),
875 @RestParameter(name = "withacl", isRequired = false, description = "Whether the acl metadata should be "
876 + "included in the response.", type = Type.BOOLEAN),
877 @RestParameter(name = "withmetadata", isRequired = false, description = "Whether the metadata catalogs "
878 + "should be included in the response.", type = Type.BOOLEAN),
879 @RestParameter(name = "withscheduling", isRequired = false, description = "Whether the scheduling "
880 + "information should be included in the response.", type = Type.BOOLEAN),
881 @RestParameter(name = "withpublications", isRequired = false, description = "Whether the publication ids and "
882 + "urls should be included in the response.", type = Type.BOOLEAN),
883 @RestParameter(name = "includeInternalPublication", description = "Whether internal publications should be "
884 + "included.", isRequired = false, type = Type.BOOLEAN),
885 @RestParameter(name = "onlyWithWriteAccess", isRequired = false, description = "Whether only to get the "
886 + "events to which we have write access.", type = Type.BOOLEAN),
887 @RestParameter(name = "filter", isRequired = false, description = "Usage [Filter Name]:[Value to Filter With]"
888 + ". Multiple filters can be used by combining them with commas \",\". Available Filters: presenters, "
889 + "contributors, location, textFilter, series, subject. If API ver > 1.1.0 also: identifier, title, "
890 + "description, series_name, language, created, license, rightsholder, is_part_of, source, status, "
891 + "agent_id, start, technical_start.", type = STRING),
892 @RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting "
893 + "criteria. In the comma seperated list each type of sorting is specified as a pair such as: "
894 + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or "
895 + "descending order and is mandatory.", isRequired = false, type = STRING),
896 @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.",
897 isRequired = false, type = RestParameter.Type.INTEGER),
898 @RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false,
899 type = RestParameter.Type.INTEGER)
900 },
901 responses = {
902 @RestResponse(description = "A (potentially empty) list of events is returned.",
903 responseCode = HttpServletResponse.SC_OK)
904 })
905 public Response getEvents(@HeaderParam("Accept") String acceptHeader, @QueryParam("id") String id,
906 @QueryParam("commentReason") String reasonFilter, @QueryParam("commentResolution") String resolutionFilter,
907 @QueryParam("filter") List<String> filter, @QueryParam("sort") String sort,
908 @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("sign") boolean sign,
909 @QueryParam("withacl") Boolean withAcl, @QueryParam("withmetadata") Boolean withMetadata,
910 @QueryParam("withscheduling") Boolean withScheduling,
911 @QueryParam("onlyWithWriteAccess") Boolean onlyWithWriteAccess,
912 @QueryParam("withpublications") Boolean withPublications,
913 @QueryParam("includeInternalPublication") Boolean includeInternalPublication) {
914 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
915 if (requestedVersion.isSmallerThan(VERSION_1_1_0)) {
916
917 withScheduling = false;
918 }
919
920 Optional<Integer> optLimit = Optional.ofNullable(limit);
921 Optional<Integer> optOffset = Optional.ofNullable(offset);
922 Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
923 EventSearchQuery query = new EventSearchQuery(getSecurityService().getOrganization().getId(),
924 getSecurityService().getUser());
925
926 if (optLimit.isPresent() && limit == 0) {
927 optLimit = Optional.empty();
928 }
929
930
931 List<IndexObject> allEvents = new ArrayList<>();
932
933 if (!isNullOrEmpty(filter)) {
934
935 if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_5_0)) {
936 filter = filter.subList(0,1);
937 }
938 for (String filterPart : filter) {
939
940
941 for (String f : filterPart.split(",")) {
942 String[] filterTuple = f.split(":");
943 if (filterTuple.length < 2) {
944 logger.debug("No value for filter {} in filters list: {}", filterTuple[0], filter);
945 continue;
946 }
947
948 String name = filterTuple[0];
949 String value;
950
951 if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_1_0)) {
952
953 value = f.substring(name.length() + 1);
954 } else {
955 value = filterTuple[1];
956 }
957
958 if ("presenters".equals(name)) {
959 query.withPresenter(value);
960 } else if ("contributors".equals(name)) {
961 query.withContributor(value);
962 } else if ("location".equals(name)) {
963 query.withLocation(value);
964 } else if ("textFilter".equals(name)) {
965 query.withText(value);
966 } else if ("series".equals(name)) {
967 query.withSeriesId(value);
968 } else if ("subject".equals(name)) {
969 query.withSubject(value);
970 } else if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_1_0)) {
971
972 if ("identifier".equals(name)) {
973 query.withIdentifier(value);
974 } else if ("title".equals(name)) {
975 query.withTitle(value);
976 } else if ("description".equals(name)) {
977 query.withDescription(value);
978 } else if ("series_name".equals(name)) {
979 query.withSeriesName(value);
980 } else if ("language".equals(name)) {
981 query.withLanguage(value);
982 } else if ("created".equals(name)) {
983 query.withCreated(value);
984 } else if ("license".equals(name)) {
985 query.withLicense(value);
986 } else if ("rightsholder".equals(name)) {
987 query.withRights(value);
988 } else if ("is_part_of".equals(name)) {
989 query.withSeriesId(value);
990 } else if ("source".equals(name)) {
991 query.withSource(value);
992 } else if ("status".equals(name)) {
993 query.withEventStatus(value);
994 } else if ("agent_id".equals(name)) {
995 query.withAgentId(value);
996 } else if ("start".equals(name)) {
997 try {
998 Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(value);
999 query.withStartFrom(fromAndToCreationRange.getA());
1000 query.withStartTo(fromAndToCreationRange.getB());
1001 } catch (Exception e) {
1002 return RestUtil.R
1003 .badRequest(String.format("Filter 'start' could not be parsed: %s", e.getMessage()));
1004
1005 }
1006 } else if ("technical_start".equals(name)) {
1007 try {
1008 Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(value);
1009 query.withTechnicalStartFrom(fromAndToCreationRange.getA());
1010 query.withTechnicalStartTo(fromAndToCreationRange.getB());
1011 } catch (Exception e) {
1012 return RestUtil.R
1013 .badRequest(String.format("Filter 'technical_start' could not be parsed: %s", e.getMessage()));
1014
1015 }
1016 } else {
1017 logger.warn("Unknown filter criteria {}", name);
1018 return RestUtil.R.badRequest(String.format("Unknown filter criterion in request: %s", name));
1019
1020 }
1021 }
1022 }
1023
1024 if (optSort.isPresent()) {
1025 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
1026 for (SortCriterion criterion : sortCriteria) {
1027
1028 switch (criterion.getFieldName()) {
1029 case EventIndexSchema.TITLE:
1030 query.sortByTitle(criterion.getOrder());
1031 break;
1032 case EventIndexSchema.PRESENTER:
1033 query.sortByPresenter(criterion.getOrder());
1034 break;
1035 case EventIndexSchema.TECHNICAL_START:
1036 case "technical_date":
1037 query.sortByTechnicalStartDate(criterion.getOrder());
1038 break;
1039 case EventIndexSchema.TECHNICAL_END:
1040 query.sortByTechnicalEndDate(criterion.getOrder());
1041 break;
1042 case EventIndexSchema.START_DATE:
1043 case "date":
1044 query.sortByStartDate(criterion.getOrder());
1045 break;
1046 case EventIndexSchema.END_DATE:
1047 query.sortByEndDate(criterion.getOrder());
1048 break;
1049 case EventIndexSchema.WORKFLOW_STATE:
1050 query.sortByWorkflowState(criterion.getOrder());
1051 break;
1052 case EventIndexSchema.SERIES_NAME:
1053 query.sortBySeriesName(criterion.getOrder());
1054 break;
1055 case EventIndexSchema.LOCATION:
1056 query.sortByLocation(criterion.getOrder());
1057 break;
1058
1059
1060 case "review_status":
1061 case "scheduling_status":
1062 break;
1063 default:
1064 return RestUtil.R.badRequest(String.format("Unknown sort criterion in request: %s",
1065 criterion.getFieldName()));
1066 }
1067 }
1068 }
1069
1070
1071 if (StringUtils.isNotBlank(resolutionFilter)) {
1072 try {
1073 CommentResolution.valueOf(resolutionFilter);
1074 } catch (Exception e) {
1075 logger.debug("Unable to parse comment resolution filter {}", resolutionFilter);
1076 return Response.status(Status.BAD_REQUEST).build();
1077 }
1078 }
1079
1080 if (optLimit.isPresent()) {
1081 query.withLimit(optLimit.get());
1082 }
1083 if (optOffset.isPresent()) {
1084 query.withOffset(offset);
1085 }
1086
1087
1088 SearchResult<Event> results = null;
1089 try {
1090 results = elasticsearchIndex.getByQuery(query);
1091 } catch (SearchIndexException e) {
1092 logger.error("The External Search Index was not able to get the events list", e);
1093 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1094 }
1095
1096 SearchResultItem<Event>[] items = results.getItems();
1097 List<IndexObject> events = new ArrayList<>();
1098 for (SearchResultItem<Event> item : items) {
1099 Event source = item.getSource();
1100 source.updatePreview(previewSubtype);
1101 events.add(source);
1102 }
1103
1104 allEvents.addAll(events);
1105 }
1106 } else {
1107 if (optSort.isPresent()) {
1108 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
1109 for (SortCriterion criterion : sortCriteria) {
1110
1111 switch (criterion.getFieldName()) {
1112 case EventIndexSchema.TITLE:
1113 query.sortByTitle(criterion.getOrder());
1114 break;
1115 case EventIndexSchema.PRESENTER:
1116 query.sortByPresenter(criterion.getOrder());
1117 break;
1118 case EventIndexSchema.TECHNICAL_START:
1119 case "technical_date":
1120 query.sortByTechnicalStartDate(criterion.getOrder());
1121 break;
1122 case EventIndexSchema.TECHNICAL_END:
1123 query.sortByTechnicalEndDate(criterion.getOrder());
1124 break;
1125 case EventIndexSchema.START_DATE:
1126 case "date":
1127 query.sortByStartDate(criterion.getOrder());
1128 break;
1129 case EventIndexSchema.END_DATE:
1130 query.sortByEndDate(criterion.getOrder());
1131 break;
1132 case EventIndexSchema.WORKFLOW_STATE:
1133 query.sortByWorkflowState(criterion.getOrder());
1134 break;
1135 case EventIndexSchema.SERIES_NAME:
1136 query.sortBySeriesName(criterion.getOrder());
1137 break;
1138 case EventIndexSchema.LOCATION:
1139 query.sortByLocation(criterion.getOrder());
1140 break;
1141
1142
1143 case "review_status":
1144 case "scheduling_status":
1145 break;
1146 default:
1147 return RestUtil.R.badRequest(String.format("Unknown sort criterion in request: %s",
1148 criterion.getFieldName()));
1149 }
1150 }
1151 }
1152
1153
1154 if (StringUtils.isNotBlank(resolutionFilter)) {
1155 try {
1156 CommentResolution.valueOf(resolutionFilter);
1157 } catch (Exception e) {
1158 logger.debug("Unable to parse comment resolution filter {}", resolutionFilter);
1159 return Response.status(Status.BAD_REQUEST).build();
1160 }
1161 }
1162
1163 if (optLimit.isPresent()) {
1164 query.withLimit(optLimit.get());
1165 }
1166 if (optOffset.isPresent()) {
1167 query.withOffset(offset);
1168 }
1169
1170 if (onlyWithWriteAccess != null && onlyWithWriteAccess) {
1171 query.withoutActions();
1172 query.withAction(Permissions.Action.WRITE);
1173 }
1174
1175
1176 SearchResult<Event> results = null;
1177 try {
1178 results = elasticsearchIndex.getByQuery(query);
1179 } catch (SearchIndexException e) {
1180 logger.error("The External Search Index was not able to get the events list", e);
1181 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1182 }
1183
1184 SearchResultItem<Event>[] items = results.getItems();
1185 List<IndexObject> events = new ArrayList<>();
1186 for (SearchResultItem<Event> item : items) {
1187 Event source = item.getSource();
1188 source.updatePreview(previewSubtype);
1189 events.add(source);
1190 }
1191
1192 allEvents.addAll(events);
1193 }
1194 try {
1195 return getJsonEvents(
1196 acceptHeader, allEvents, withAcl, withMetadata, withScheduling, withPublications, includeInternalPublication,
1197 sign, requestedVersion);
1198 } catch (Exception e) {
1199 logger.error("Unable to get events", e);
1200 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1201 }
1202 }
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226 protected Response getJsonEvents(String acceptHeader, List<IndexObject> events, Boolean withAcl, Boolean withMetadata,
1227 Boolean withScheduling, Boolean withPublications, Boolean includeInternalPublication, Boolean withSignedUrls,
1228 ApiVersion requestedVersion)
1229 throws IndexServiceException, UnauthorizedException, SchedulerException {
1230 JsonArray eventsArray = new JsonArray();
1231 for (IndexObject item : events) {
1232 JsonObject jsonEvent = eventToJSON((Event) item, withAcl, withMetadata, withScheduling, withPublications,
1233 includeInternalPublication, withSignedUrls, requestedVersion);
1234 eventsArray.add(jsonEvent);
1235 }
1236
1237 return ApiResponseBuilder.Json.ok(requestedVersion, eventsArray);
1238 }
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261 protected JsonObject eventToJSON(Event event, Boolean withAcl, Boolean withMetadata, Boolean withScheduling,
1262 Boolean withPublications, Boolean includeInternalPublication, Boolean withSignedUrls,
1263 ApiVersion requestedVersion) throws IndexServiceException, SchedulerException, UnauthorizedException {
1264 JsonObject json = new JsonObject();
1265
1266 if (event.getArchiveVersion() != null) {
1267 json.addProperty("archive_version", event.getArchiveVersion());
1268 }
1269 json.addProperty("created", safeString(event.getCreated()));
1270 json.addProperty("creator", safeString(event.getCreator()));
1271 json.add("contributor", collectionToJsonArray(event.getContributors()));
1272 json.addProperty("description", safeString(event.getDescription()));
1273 json.addProperty("has_previews", event.hasPreview());
1274 json.addProperty("identifier", safeString(event.getIdentifier()));
1275 json.addProperty("location", safeString(event.getLocation()));
1276 json.add("presenter", collectionToJsonArray(event.getPresenters()));
1277
1278 if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
1279 json.addProperty("language", safeString(event.getLanguage()));
1280 json.addProperty("rightsholder", safeString(event.getRights()));
1281 json.addProperty("license", safeString(event.getLicense()));
1282 json.addProperty("is_part_of", safeString(event.getSeriesId()));
1283 json.addProperty("series", safeString(event.getSeriesName()));
1284 json.addProperty("source", safeString(event.getSource()));
1285 json.addProperty("status", safeString(event.getEventStatus()));
1286 }
1287
1288 JsonArray publicationIds = new JsonArray();
1289 if (event.getPublications() != null) {
1290 for (Publication publication : event.getPublications()) {
1291 publicationIds.add(new JsonPrimitive(publication.getChannel()));
1292 }
1293 }
1294 json.add("publication_status", publicationIds);
1295 json.addProperty("processing_state", safeString(event.getWorkflowState()));
1296
1297 if (requestedVersion.isSmallerThan(VERSION_1_4_0)) {
1298 json.addProperty("start", safeString(event.getTechnicalStartTime()));
1299 if (event.getTechnicalEndTime() != null) {
1300 long duration = new DateTime(event.getTechnicalEndTime()).getMillis()
1301 - new DateTime(event.getTechnicalStartTime()).getMillis();
1302 json.addProperty("duration", duration);
1303 }
1304 } else {
1305 json.addProperty("start", safeString(event.getRecordingStartDate()));
1306 if (event.getDuration() != null) {
1307 json.addProperty("duration", event.getDuration());
1308 } else {
1309 json.add("duration", JsonNull.INSTANCE);
1310 }
1311 }
1312
1313 if (StringUtils.trimToNull(event.getSubject()) != null) {
1314 json.add("subjects", splitSubjectIntoArray(event.getSubject()));
1315 } else {
1316 json.add("subjects", new JsonArray());
1317 }
1318
1319 json.addProperty("title", safeString(event.getTitle()));
1320
1321 if (withAcl != null && withAcl) {
1322 AccessControlList acl = getAclFromEvent(event);
1323 json.add("acl", AclUtils.serializeAclToJson(acl));
1324 }
1325
1326 if (withMetadata != null && withMetadata) {
1327 try {
1328 Optional<MetadataList> metadata = getEventMetadata(event);
1329 if (metadata.isPresent()) {
1330 json.add("metadata", MetadataJson.listToJson(metadata.get(), true));
1331 }
1332 } catch (Exception e) {
1333 logger.error("Unable to get metadata for event '{}'", event.getIdentifier(), e);
1334 throw new IndexServiceException("Unable to add metadata to event", e);
1335 }
1336 }
1337
1338 if (withScheduling != null && withScheduling) {
1339 json.add("scheduling", SchedulingInfo.of(event.getIdentifier(), schedulerService).toJson());
1340 }
1341
1342 if (withPublications != null && withPublications) {
1343 List<JsonObject> publications = getPublications(event, withSignedUrls, includeInternalPublication,
1344 requestedVersion);
1345 JsonArray pubDetails = new JsonArray();
1346 for (JsonObject pub : publications) {
1347 pubDetails.add(pub);
1348 }
1349 json.add("publications", pubDetails);
1350 }
1351
1352 return json;
1353 }
1354
1355 private JsonArray splitSubjectIntoArray(final String subject) {
1356 JsonArray array = new JsonArray();
1357 if (subject != null && !subject.trim().isEmpty()) {
1358 for (String part : subject.split(",")) {
1359 array.add(new JsonPrimitive(part.trim()));
1360 }
1361 }
1362 return array;
1363 }
1364
1365 @GET
1366 @Path("{eventId}/acl")
1367 @RestQuery(
1368 name = "geteventacl",
1369 description = "Returns an event's access policy.",
1370 returnDescription = "",
1371 pathParameters = {
1372 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1373 },
1374 responses = {
1375 @RestResponse(description = "The access control list for the specified event is returned.",
1376 responseCode = HttpServletResponse.SC_OK),
1377 @RestResponse(description = "The specified event does not exist.",
1378 responseCode = HttpServletResponse.SC_NOT_FOUND)
1379 })
1380 public Response getEventAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
1381 throws Exception {
1382 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1383 if (eventOpt.isPresent()) {
1384 AccessControlList acl = getAclFromEvent(eventOpt.get());
1385 return ApiResponseBuilder.Json.ok(acceptHeader, AclUtils.serializeAclToJson(acl));
1386 }
1387 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1388 }
1389
1390 @PUT
1391 @Path("{eventId}/acl")
1392 @RestQuery(
1393 name = "updateeventacl",
1394 description = "Update an event's access policy.",
1395 returnDescription = "",
1396 pathParameters = {
1397 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1398 },
1399 restParameters = {
1400 @RestParameter(name = "acl", isRequired = true, description = "Access policy", type = STRING)
1401 },
1402 responses = {
1403 @RestResponse(description = "The access control list for the specified event is updated.",
1404 responseCode = HttpServletResponse.SC_NO_CONTENT),
1405 @RestResponse(description = "The specified event does not exist.",
1406 responseCode = HttpServletResponse.SC_NOT_FOUND)
1407 })
1408 public Response updateEventAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1409 @FormParam("acl") String acl) throws Exception {
1410 if (indexService.getEvent(id, elasticsearchIndex).isPresent()) {
1411 AccessControlList accessControlList;
1412 try {
1413 accessControlList = AclUtils.deserializeJsonToAcl(acl, false);
1414 } catch (ParseException e) {
1415 logger.debug("Unable to update event acl to '{}'", acl, e);
1416 return R.badRequest(String.format("Unable to parse acl '%s' because '%s'", acl, e.getMessage()));
1417 } catch (IllegalArgumentException e) {
1418 logger.debug("Unable to update event acl to '{}'", acl, e);
1419 return R.badRequest(e.getMessage());
1420 }
1421 try {
1422 accessControlList = indexService.updateEventAcl(id, accessControlList, elasticsearchIndex);
1423 } catch (IllegalArgumentException e) {
1424 logger.error("Unable to update event '{}' acl with '{}'", id, acl, e);
1425 return Response.status(Status.FORBIDDEN).build();
1426 }
1427 return Response.noContent().build();
1428 } else {
1429 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1430 }
1431 }
1432
1433 @POST
1434 @Path("{eventId}/acl/{action}")
1435 @RestQuery(
1436 name = "addeventace",
1437 description = "Grants permission to execute action on the specified event to any user with role role. Note that "
1438 + "this is a convenience method to avoid having to build and post a complete access control list.",
1439 returnDescription = "",
1440 pathParameters = {
1441 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
1442 @RestParameter(name = "action", description = "The action that is allowed to be executed", isRequired = true,
1443 type = STRING)
1444 },
1445 restParameters = {
1446 @RestParameter(name = "role", isRequired = true, description = "The role that is granted permission",
1447 type = STRING)
1448 },
1449 responses = {
1450 @RestResponse(description = "The permission has been created in the access control list of the specified "
1451 + "event.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1452 @RestResponse(description = "The specified event does not exist.",
1453 responseCode = HttpServletResponse.SC_NOT_FOUND)
1454 })
1455 public Response addEventAce(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1456 @PathParam("action") String action, @FormParam("role") String role) throws Exception {
1457 List<AccessControlEntry> entries = new ArrayList<>();
1458 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1459 if (eventOpt.isPresent()) {
1460 AccessControlList accessControlList = getAclFromEvent(eventOpt.get());
1461 AccessControlEntry newAce = new AccessControlEntry(role, action, true);
1462 boolean alreadyInAcl = false;
1463 for (AccessControlEntry ace : accessControlList.getEntries()) {
1464 if (ace.equals(newAce)) {
1465
1466 entries = accessControlList.getEntries();
1467 alreadyInAcl = true;
1468 break;
1469 } else if (ace.getAction().equals(newAce.getAction()) && ace.getRole().equals(newAce.getRole())
1470 && !ace.isAllow()) {
1471 entries.add(newAce);
1472 alreadyInAcl = true;
1473 } else {
1474 entries.add(ace);
1475 }
1476 }
1477
1478 if (!alreadyInAcl) {
1479 entries.add(newAce);
1480 }
1481
1482 AccessControlList withNewAce = new AccessControlList(entries);
1483 try {
1484 withNewAce = indexService.updateEventAcl(id, withNewAce, elasticsearchIndex);
1485 } catch (IllegalArgumentException e) {
1486 logger.error("Unable to update event '{}' acl entry with action '{}' and role '{}'", id, action, role, e);
1487 return Response.status(Status.FORBIDDEN).build();
1488 }
1489 return Response.noContent().build();
1490 }
1491 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1492 }
1493
1494 @DELETE
1495 @Path("{eventId}/acl/{action}/{role}")
1496 @RestQuery(
1497 name = "deleteeventace",
1498 description = "Revokes permission to execute action on the specified event from any user with role role.",
1499 returnDescription = "",
1500 pathParameters = {
1501 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
1502 @RestParameter(name = "action", description = "The action that is no longer allowed to be executed",
1503 isRequired = true, type = STRING),
1504 @RestParameter(name = "role", description = "The role that is no longer granted permission",
1505 isRequired = true, type = STRING)
1506 },
1507 responses = {
1508 @RestResponse(description = "The permission has been revoked from the access control list of the specified "
1509 + "event.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1510 @RestResponse(description = "The specified event does not exist.",
1511 responseCode = HttpServletResponse.SC_NOT_FOUND)
1512 })
1513 public Response deleteEventAce(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1514 @PathParam("action") String action, @PathParam("role") String role) throws Exception {
1515 List<AccessControlEntry> entries = new ArrayList<>();
1516 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1517 if (eventOpt.isPresent()) {
1518 AccessControlList accessControlList = getAclFromEvent(eventOpt.get());
1519 boolean foundDelete = false;
1520 for (AccessControlEntry ace : accessControlList.getEntries()) {
1521 if (ace.getAction().equals(action) && ace.getRole().equals(role)) {
1522 foundDelete = true;
1523 } else {
1524 entries.add(ace);
1525 }
1526 }
1527
1528 if (!foundDelete) {
1529 return ApiResponseBuilder.notFound("Unable to find an access control entry with action '%s' and role '%s'",
1530 action, role);
1531 }
1532
1533 AccessControlList withoutDeleted = new AccessControlList(entries);
1534 try {
1535 withoutDeleted = indexService.updateEventAcl(id, withoutDeleted, elasticsearchIndex);
1536 } catch (IllegalArgumentException e) {
1537 logger.error("Unable to delete event's '{}' acl entry with action '{}' and role '{}'", id, action, role, e);
1538 return Response.status(Status.FORBIDDEN).build();
1539 }
1540 return Response.noContent().build();
1541 }
1542 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1543 }
1544
1545 @GET
1546 @Path("{eventId}/metadata")
1547 @RestQuery(
1548 name = "geteventmetadata",
1549 description = "Returns the event's metadata of the specified type. For a metadata catalog there is the flavor "
1550 + "such as 'dublincore/episode' and this is the unique type.",
1551 returnDescription = "",
1552 pathParameters = {
1553 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1554 },
1555 restParameters = {
1556 @RestParameter(name = "type", isRequired = false, description = "The type of metadata to get", type = STRING)
1557 },
1558 responses = {
1559 @RestResponse(description = "The metadata collection is returned.", responseCode = HttpServletResponse.SC_OK),
1560 @RestResponse(description = "The specified event does not exist.",
1561 responseCode = HttpServletResponse.SC_NOT_FOUND)
1562 })
1563 public Response getAllEventMetadata(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1564 @QueryParam("type") String type) throws Exception {
1565 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
1566 if (StringUtils.trimToNull(type) == null) {
1567 Optional<MetadataList> metadataList = getEventMetadataById(id);
1568 if (metadataList.isPresent()) {
1569 MetadataList actualList = metadataList.get();
1570
1571
1572
1573 final DublinCoreMetadataCollection collection = actualList.getMetadataByFlavor("dublincore/episode");
1574 final boolean withOrderedText = collection == null;
1575 if (collection != null) {
1576 convertStartDateTimeToApiV1(collection);
1577 }
1578
1579 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.listToJson(actualList, withOrderedText));
1580 }
1581 else {
1582 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1583 }
1584 } else {
1585 return getEventMetadataByType(id, type, requestedVersion);
1586 }
1587 }
1588
1589 private void convertStartDateTimeToApiV1(DublinCoreMetadataCollection collection) throws java.text.ParseException {
1590
1591 if (!collection.getOutputFields().containsKey("startDate")) {
1592 return;
1593 }
1594
1595 MetadataField oldStartDateField = collection.getOutputFields().get("startDate");
1596 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(oldStartDateField.getPattern());
1597 Date startDate = sdf.parse((String) oldStartDateField.getValue());
1598
1599 if (configuredMetadataFields.containsKey("startDate")) {
1600 MetadataField startDateField = configuredMetadataFields.get("startDate");
1601 final String pattern = startDateField.getPattern() == null ? "yyyy-MM-dd" : startDateField.getPattern();
1602 startDateField = new MetadataField(startDateField);
1603 startDateField.setPattern(pattern);
1604 sdf.applyPattern(startDateField.getPattern());
1605 startDateField.setValue(sdf.format(startDate));
1606 collection.removeField(oldStartDateField);
1607 collection.addField(startDateField);
1608 }
1609
1610 if (configuredMetadataFields.containsKey("startTime")) {
1611 MetadataField startTimeField = configuredMetadataFields.get("startTime");
1612 final String pattern = startTimeField.getPattern() == null ? "HH:mm" : startTimeField.getPattern();
1613 startTimeField = new MetadataField(startTimeField);
1614 startTimeField.setPattern(pattern);
1615 sdf.applyPattern(startTimeField.getPattern());
1616 startTimeField.setValue(sdf.format(startDate));
1617 collection.addField(startTimeField);
1618 }
1619 }
1620
1621 protected Optional<MetadataList> getEventMetadataById(String id) throws IndexServiceException, Exception {
1622 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1623 if (eventOpt.isPresent()) {
1624 return getEventMetadata(eventOpt.get());
1625 }
1626 return Optional.<MetadataList> empty();
1627 }
1628
1629 protected Optional<MetadataList> getEventMetadata(Event event) throws IndexServiceException, Exception {
1630 MetadataList metadataList = new MetadataList();
1631 List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
1632 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1633 catalogUIAdapters.remove(eventCatalogUIAdapter);
1634 if (catalogUIAdapters.size() > 0) {
1635 MediaPackage mediaPackage = indexService.getEventMediapackage(event);
1636 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1637
1638 DublinCoreMetadataCollection fields = catalogUIAdapter.getFields(mediaPackage);
1639 if (fields != null) {
1640 ExternalMetadataUtils.removeCollectionList(fields);
1641 metadataList.add(catalogUIAdapter, fields);
1642 }
1643 }
1644 }
1645 DublinCoreMetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter,
1646 new EmptyResourceListQuery());
1647 ExternalMetadataUtils.changeSubjectToSubjects(collection);
1648 ExternalMetadataUtils.removeCollectionList(collection);
1649 metadataList.add(eventCatalogUIAdapter, collection);
1650 if (WorkflowInstance.WorkflowState.RUNNING.toString().equals(event.getWorkflowState())) {
1651 metadataList.setLocked(Locked.WORKFLOW_RUNNING);
1652 }
1653 return Optional.of(metadataList);
1654 }
1655
1656 private Optional<MediaPackageElementFlavor> getFlavor(String flavorString) {
1657 try {
1658 MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
1659 return Optional.of(flavor);
1660 } catch (IllegalArgumentException e) {
1661 return Optional.empty();
1662 }
1663 }
1664
1665 private Response getEventMetadataByType(String id, String type, ApiVersion requestedVersion) throws Exception {
1666 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1667 if (eventOpt.isPresent()) {
1668 Event event = eventOpt.get();
1669 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
1670 if (flavor.isEmpty()) {
1671 return R.badRequest(
1672 String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
1673 }
1674
1675 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1676 if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
1677 DublinCoreMetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter,
1678 new EmptyResourceListQuery());
1679 ExternalMetadataUtils.changeSubjectToSubjects(collection);
1680 ExternalMetadataUtils.removeCollectionList(collection);
1681 convertStartDateTimeToApiV1(collection);
1682 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.collectionToJson(collection, false));
1683 }
1684
1685 List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
1686 catalogUIAdapters.remove(eventCatalogUIAdapter);
1687 if (catalogUIAdapters.size() > 0) {
1688 MediaPackage mediaPackage = indexService.getEventMediapackage(event);
1689 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1690 if (flavor.get().equals(catalogUIAdapter.getFlavor())) {
1691 DublinCoreMetadataCollection fields = catalogUIAdapter.getFields(mediaPackage);
1692 ExternalMetadataUtils.removeCollectionList(fields);
1693 convertStartDateTimeToApiV1(fields);
1694 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.collectionToJson(fields, false));
1695 }
1696 }
1697 }
1698 return ApiResponseBuilder.notFound("Cannot find a catalog with type '%s' for event with id '%s'.", type, id);
1699 }
1700 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1701 }
1702
1703 @PUT
1704 @Path("{eventId}/metadata")
1705 @RestQuery(
1706 name = "updateeventmetadata",
1707 description = "Update the metadata with the matching type of the specified event. For a metadata catalog there "
1708 + "is the flavor such as 'dublincore/episode' and this is the unique type.",
1709 returnDescription = "",
1710 pathParameters = {
1711 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1712 },
1713 restParameters = {
1714 @RestParameter(name = "type", isRequired = true, description = "The type of metadata to update",
1715 type = STRING),
1716 @RestParameter(name = "metadata", description = "Metadata catalog in JSON format", isRequired = true,
1717 type = STRING)
1718 },
1719 responses = {
1720 @RestResponse(description = "The metadata of the given namespace has been updated.",
1721 responseCode = HttpServletResponse.SC_OK),
1722 @RestResponse(description = "The request is invalid or inconsistent.",
1723 responseCode = HttpServletResponse.SC_BAD_REQUEST),
1724 @RestResponse(description = "The specified event does not exist.",
1725 responseCode = HttpServletResponse.SC_NOT_FOUND)
1726 })
1727 public Response updateEventMetadataByType(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1728 @QueryParam("type") String type, @FormParam("metadata") String metadataJSON) throws Exception {
1729 Map<String, String> updatedFields;
1730 JSONParser parser = new JSONParser();
1731 try {
1732 updatedFields = RequestUtils.getKeyValueMap(metadataJSON);
1733 } catch (ParseException e) {
1734 logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
1735 return RestUtil.R.badRequest(String.format("Unable to parse metadata fields as json from '%s'", metadataJSON));
1736 } catch (IllegalArgumentException e) {
1737 logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
1738 return RestUtil.R.badRequest(e.getMessage());
1739 }
1740
1741 if (updatedFields == null || updatedFields.size() == 0) {
1742 return RestUtil.R.badRequest(
1743 String.format("Unable to parse metadata fields as json from '%s' because there were no fields to update.",
1744 metadataJSON));
1745 }
1746
1747 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
1748 if (flavor.isEmpty()) {
1749 return R.badRequest(
1750 String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
1751 }
1752
1753 DublinCoreMetadataCollection collection = null;
1754 EventCatalogUIAdapter adapter = null;
1755 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1756 if (eventOpt.isPresent()) {
1757 Event event = eventOpt.get();
1758 MetadataList metadataList = new MetadataList();
1759
1760 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1761 if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
1762 collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter);
1763 adapter = eventCatalogUIAdapter;
1764 } else {
1765 metadataList.add(eventCatalogUIAdapter, EventUtils.getEventMetadata(event, eventCatalogUIAdapter));
1766 }
1767
1768
1769 List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
1770 catalogUIAdapters.remove(eventCatalogUIAdapter);
1771 if (catalogUIAdapters.size() > 0) {
1772 MediaPackage mediaPackage = indexService.getEventMediapackage(event);
1773 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1774 if (flavor.get().equals(catalogUIAdapter.getFlavor())) {
1775 collection = catalogUIAdapter.getFields(mediaPackage);
1776 adapter = eventCatalogUIAdapter;
1777 } else {
1778 metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(mediaPackage));
1779 }
1780 }
1781 }
1782
1783 if (collection == null) {
1784 return ApiResponseBuilder.notFound("Cannot find a catalog with type '%s' for event with id '%s'.", type, id);
1785 }
1786
1787 for (String key : updatedFields.keySet()) {
1788 if ("subjects".equals(key)) {
1789 MetadataField field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
1790 Optional<Response> error = validateField(field, key, id, type, updatedFields);
1791 if (error.isPresent()) {
1792 return error.get();
1793 }
1794 collection.removeField(field);
1795 JSONArray subjectArray = (JSONArray) parser.parse(updatedFields.get(key));
1796 collection.addField(
1797 MetadataJson.copyWithDifferentJsonValue(field, StringUtils.join(subjectArray.iterator(), ",")));
1798 } else if ("startDate".equals(key)) {
1799
1800 MetadataField field = collection.getOutputFields().get(key);
1801 Optional<Response> error = validateField(field, key, id, type, updatedFields);
1802 if (error.isPresent()) {
1803 return error.get();
1804 }
1805 String apiPattern = field.getPattern();
1806 if (configuredMetadataFields.containsKey("startDate")) {
1807 final String startDate = configuredMetadataFields.get("startDate").getPattern();
1808 apiPattern = startDate == null ? apiPattern : startDate;
1809 }
1810 SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(apiPattern);
1811 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
1812 DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
1813 DateTime newStartDate = new DateTime(apiSdf.parse(updatedFields.get(key)), DateTimeZone.UTC);
1814 DateTime updatedStartDate = oldStartDate.withDate(newStartDate.year().get(), newStartDate.monthOfYear().get(),
1815 newStartDate.dayOfMonth().get());
1816 collection.removeField(field);
1817 collection.addField(
1818 MetadataJson.copyWithDifferentJsonValue(field, sdf.format(updatedStartDate.toDate())));
1819 } else if ("startTime".equals(key)) {
1820
1821 MetadataField field = collection.getOutputFields().get("startDate");
1822 Optional<Response> error = validateField(field, "startDate", id, type, updatedFields);
1823 if (error.isPresent()) {
1824 return error.get();
1825 }
1826 String apiPattern = "HH:mm";
1827 if (configuredMetadataFields.containsKey("startTime")) {
1828 final String startTime = configuredMetadataFields.get("startTime").getPattern();
1829 apiPattern = startTime == null ? apiPattern : startTime;
1830 }
1831 SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(apiPattern);
1832 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
1833 DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
1834 DateTime newStartDate = new DateTime(apiSdf.parse(updatedFields.get(key)), DateTimeZone.UTC);
1835 DateTime updatedStartDate = oldStartDate.withTime(
1836 newStartDate.hourOfDay().get(),
1837 newStartDate.minuteOfHour().get(),
1838 newStartDate.secondOfMinute().get(),
1839 newStartDate.millisOfSecond().get());
1840 collection.removeField(field);
1841 collection.addField(
1842 MetadataJson.copyWithDifferentJsonValue(field, sdf.format(updatedStartDate.toDate())));
1843 } else {
1844 MetadataField field = collection.getOutputFields().get(key);
1845 Optional<Response> error = validateField(field, key, id, type, updatedFields);
1846 if (error.isPresent()) {
1847 return error.get();
1848 }
1849 collection.removeField(field);
1850 collection.addField(
1851 MetadataJson.copyWithDifferentJsonValue(field, updatedFields.get(key)));
1852 }
1853 }
1854
1855 metadataList.add(adapter, collection);
1856 indexService.updateEventMetadata(id, metadataList, elasticsearchIndex);
1857 return Response.noContent().build();
1858 }
1859 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1860 }
1861
1862 private Optional<Response> validateField(MetadataField field, String key, String id, String type,
1863 Map<String, String> updatedFields) {
1864 if (field == null) {
1865 return Optional.of(ApiResponseBuilder.notFound(
1866 "Cannot find a metadata field with id '%s' from event with id '%s' and the metadata type '%s'.",
1867 key, id, type));
1868 } else if (field.isRequired() && StringUtils.isBlank(updatedFields.get(key))) {
1869 return Optional.of(R.badRequest(String.format(
1870 "The event metadata field with id '%s' and the metadata type '%s' is required and can not be empty!.",
1871 key, type)));
1872 }
1873 return Optional.empty();
1874 }
1875
1876 @DELETE
1877 @Path("{eventId}/metadata")
1878 @RestQuery(
1879 name = "deleteeventmetadata",
1880 description = "Delete the metadata namespace catalog of the specified event. This will remove all fields and "
1881 + "values of the catalog.",
1882 returnDescription = "",
1883 pathParameters = {
1884 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1885 },
1886 restParameters = {
1887 @RestParameter(name = "type", isRequired = true, description = "The type of metadata to delete",
1888 type = STRING)
1889 },
1890 responses = {
1891 @RestResponse(description = "The metadata of the given namespace has been updated.",
1892 responseCode = HttpServletResponse.SC_NO_CONTENT),
1893 @RestResponse(description = "The main metadata catalog dublincore/episode cannot be deleted as it has "
1894 + "mandatory fields.", responseCode = HttpServletResponse.SC_FORBIDDEN),
1895 @RestResponse(description = "The specified event does not exist.",
1896 responseCode = HttpServletResponse.SC_NOT_FOUND)
1897 })
1898 public Response deleteEventMetadataByType(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1899 @QueryParam("type") String type) throws SearchIndexException {
1900 Optional<Event> eventOpt = indexService.getEvent(id, elasticsearchIndex);
1901 if (eventOpt.isPresent()) {
1902 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
1903 if (flavor.isEmpty()) {
1904 return R.badRequest(
1905 String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
1906 }
1907 EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
1908 if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
1909 return Response
1910 .status(Status.FORBIDDEN).entity(String
1911 .format("Unable to delete mandatory metadata catalog with type '%s' for event '%s'", type, id))
1912 .build();
1913 }
1914 try {
1915 indexService.removeCatalogByFlavor(eventOpt.get(), flavor.get());
1916 } catch (NotFoundException e) {
1917 return ApiResponseBuilder.notFound(e.getMessage());
1918 } catch (IndexServiceException e) {
1919 logger.error("Unable to remove metadata catalog with type '{}' from event '{}'", type, id, e);
1920 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1921 } catch (IllegalStateException e) {
1922 logger.debug("Unable to remove metadata catalog with type '{}' from event '{}'", type, id, e);
1923 throw new WebApplicationException(e, Status.BAD_REQUEST);
1924 } catch (UnauthorizedException e) {
1925 return Response.status(Status.UNAUTHORIZED).build();
1926 }
1927 return Response.noContent().build();
1928 }
1929 return ApiResponseBuilder.notFound("Cannot find an event with id '%s'.", id);
1930 }
1931
1932 @GET
1933 @Path("{eventId}/publications")
1934 @RestQuery(
1935 name = "geteventpublications",
1936 description = "Returns an event's list of publications.",
1937 returnDescription = "",
1938 pathParameters = {
1939 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
1940 },
1941 restParameters = {
1942 @RestParameter(name = "sign", description = "Whether public distribution urls should be signed.",
1943 isRequired = false, type = Type.BOOLEAN),
1944 @RestParameter(name = "includeInternalPublication", description = "Whether internal publications should be "
1945 + "included.", isRequired = false, type = Type.BOOLEAN)
1946 },
1947 responses = {
1948 @RestResponse(description = "The list of publications is returned.",
1949 responseCode = HttpServletResponse.SC_OK),
1950 @RestResponse(description = "The specified event does not exist.",
1951 responseCode = HttpServletResponse.SC_NOT_FOUND)
1952 })
1953
1954 public Response getEventPublications(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
1955 @QueryParam("sign") boolean sign, @QueryParam("includeInternalPublication") boolean includeInternalPublication)
1956 throws Exception {
1957 try {
1958 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
1959 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
1960 if (event.isPresent()) {
1961 JsonArray jsonArray = new JsonArray();
1962 for (JsonElement pub : getPublications(event.get(), sign, includeInternalPublication, requestedVersion)) {
1963 jsonArray.add(pub);
1964 }
1965 return ApiResponseBuilder.Json.ok(acceptHeader, jsonArray);
1966 } else {
1967 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
1968 }
1969 } catch (SearchIndexException e) {
1970 logger.error("Unable to get list of publications from event with id '{}'", id, e);
1971 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1972 }
1973 }
1974
1975 private List<JsonObject> getPublications(Event event, Boolean withSignedUrls, Boolean includeInternalPublication,
1976 ApiVersion requestedVersion) {
1977 return event.getPublications().stream()
1978 .filter(publication -> {
1979 boolean isInternalAllowed = includeInternalPublication != null && includeInternalPublication
1980 && !requestedVersion.isSmallerThan(VERSION_1_11_0);
1981 return isInternalAllowed || EventUtils.internalChannelFilter.test(publication);
1982 })
1983 .map(p -> getPublication(p, withSignedUrls, requestedVersion))
1984 .collect(Collectors.toList());
1985 }
1986
1987 public JsonObject getPublication(Publication publication, Boolean sign, ApiVersion requestedVersion) {
1988
1989 URI publicationUrl = publication.getURI();
1990 if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
1991 publicationUrl = getSignedUrl(publicationUrl, sign);
1992 }
1993
1994 JsonObject json = new JsonObject();
1995 json.addProperty("id", publication.getIdentifier());
1996 json.addProperty("channel", publication.getChannel());
1997 json.addProperty("mediatype", safeString(publication.getMimeType()));
1998 json.addProperty("url", publicationUrl != null ? publicationUrl.toString() : "");
1999 JsonArray mediaArray = new JsonArray();
2000 for (JsonObject trackJson : getPublicationTracksJson(publication, sign, requestedVersion)) {
2001 mediaArray.add(trackJson);
2002 }
2003 json.add("media", mediaArray);
2004 JsonArray attachmentArray = new JsonArray();
2005 for (JsonObject attachmentJson : getPublicationAttachmentsJson(publication, sign)) {
2006 attachmentArray.add(attachmentJson);
2007 }
2008 json.add("attachments", attachmentArray);
2009 JsonArray metadataArray = new JsonArray();
2010 for (JsonObject catalogJson : getPublicationCatalogsJson(publication, sign)) {
2011 metadataArray.add(catalogJson);
2012 }
2013 json.add("metadata", metadataArray);
2014
2015 return json;
2016 }
2017
2018 private URI getSignedUrl(URI url, boolean sign) {
2019 if (url == null || !sign) {
2020 return url;
2021 }
2022
2023 if (urlSigningService.accepts(url.toString())) {
2024 try {
2025 return URI.create(urlSigningService.sign(url.toString(), expireSeconds, null, null));
2026 } catch (UrlSigningException e) {
2027 logger.error("Unable to sign URI {}", url, e);
2028 }
2029 }
2030 return url;
2031 }
2032
2033 private List<JsonObject> getPublicationTracksJson(Publication publication, Boolean sign,
2034 ApiVersion requestedVersion) {
2035 List<JsonObject> tracksJson = new ArrayList<>();
2036
2037 for (Track track : publication.getTracks()) {
2038 JsonObject trackJson = new JsonObject();
2039
2040 trackJson.addProperty("id", safeString(track.getIdentifier()));
2041 trackJson.addProperty("mediatype", safeString(track.getMimeType()));
2042 trackJson.addProperty("url", safeString(getSignedUrl(track.getURI(), sign)));
2043 trackJson.addProperty("flavor", safeString(track.getFlavor()));
2044 trackJson.addProperty("size", track.getSize());
2045 trackJson.addProperty("checksum", safeString(track.getChecksum()));
2046 trackJson.add("tags", arrayToJsonArray(track.getTags()));
2047 trackJson.addProperty("has_audio", track.hasAudio());
2048 trackJson.addProperty("has_video", track.hasVideo());
2049 trackJson.addProperty("duration", track.getDuration() != null ? track.getDuration() : null);
2050 trackJson.addProperty("description", safeString(track.getDescription()));
2051
2052 VideoStream[] videoStreams = TrackSupport.byType(track.getStreams(), VideoStream.class);
2053 if (videoStreams.length > 0) {
2054
2055 VideoStream videoStream = videoStreams[0];
2056 if (videoStream.getBitRate() != null) {
2057 trackJson.addProperty("bitrate", videoStream.getBitRate());
2058 }
2059 if (videoStream.getFrameRate() != null) {
2060 trackJson.addProperty("framerate", videoStream.getFrameRate());
2061 }
2062 if (videoStream.getFrameCount() != null) {
2063 trackJson.addProperty("framecount", videoStream.getFrameCount());
2064 }
2065 if (videoStream.getFrameWidth() != null) {
2066 trackJson.addProperty("width", videoStream.getFrameWidth());
2067 }
2068 if (videoStream.getFrameHeight() != null) {
2069 trackJson.addProperty("height", videoStream.getFrameHeight());
2070 }
2071 }
2072
2073 if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
2074 trackJson.addProperty("is_master_playlist", track.isMaster());
2075 trackJson.addProperty("is_live", track.isLive());
2076 }
2077
2078 tracksJson.add(trackJson);
2079 }
2080
2081 return tracksJson;
2082 }
2083
2084 private List<JsonObject> getPublicationAttachmentsJson(Publication publication, Boolean sign) {
2085 List<JsonObject> attachmentsJson = new ArrayList<>();
2086
2087 for (Attachment attachment : publication.getAttachments()) {
2088 JsonObject json = new JsonObject();
2089
2090 json.addProperty("id", safeString(attachment.getIdentifier()));
2091 json.addProperty("mediatype", safeString(attachment.getMimeType()));
2092 json.addProperty("url", safeString(getSignedUrl(attachment.getURI(), sign)));
2093 json.addProperty("flavor", safeString(attachment.getFlavor()));
2094 json.addProperty("ref", safeString(attachment.getReference()));
2095 json.addProperty("size", attachment.getSize());
2096 json.addProperty("checksum", safeString(attachment.getChecksum()));
2097 json.add("tags", arrayToJsonArray(attachment.getTags()));
2098
2099 attachmentsJson.add(json);
2100 }
2101
2102 return attachmentsJson;
2103 }
2104
2105 private List<JsonObject> getPublicationCatalogsJson(Publication publication, Boolean sign) {
2106 List<JsonObject> catalogsJson = new ArrayList<>();
2107
2108 for (Catalog catalog : publication.getCatalogs()) {
2109 JsonObject json = new JsonObject();
2110
2111 json.addProperty("id", safeString(catalog.getIdentifier()));
2112 json.addProperty("mediatype", safeString(catalog.getMimeType()));
2113 json.addProperty("url", safeString(getSignedUrl(catalog.getURI(), sign)));
2114 json.addProperty("flavor", safeString(catalog.getFlavor()));
2115 json.addProperty("size", catalog.getSize());
2116 json.addProperty("checksum", safeString(catalog.getChecksum()));
2117 json.add("tags", arrayToJsonArray(catalog.getTags()));
2118
2119 catalogsJson.add(json);
2120 }
2121
2122 return catalogsJson;
2123 }
2124
2125 @GET
2126 @Path("{eventId}/publications/{publicationId}")
2127 @RestQuery(
2128 name = "geteventpublication",
2129 description = "Returns a single publication.",
2130 returnDescription = "",
2131 pathParameters = {
2132 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
2133 @RestParameter(name = "publicationId", description = "The publication id", isRequired = true, type = STRING)
2134 },
2135 restParameters = {
2136 @RestParameter(name = "sign", description = "Whether public distribution urls should be signed.",
2137 isRequired = false, type = Type.BOOLEAN)
2138 },
2139 responses = {
2140 @RestResponse(description = "The track details are returned.", responseCode = HttpServletResponse.SC_OK),
2141 @RestResponse(description = "The specified event or publication does not exist.",
2142 responseCode = HttpServletResponse.SC_NOT_FOUND)
2143 })
2144
2145 public Response getEventPublication(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String eventId,
2146 @PathParam("publicationId") String publicationId, @QueryParam("sign") boolean sign) throws Exception {
2147 try {
2148 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
2149 return ApiResponseBuilder.Json.ok(acceptHeader, getPublication(eventId, publicationId, sign, requestedVersion));
2150 } catch (NotFoundException e) {
2151 return ApiResponseBuilder.notFound(e.getMessage());
2152 } catch (SearchIndexException e) {
2153 logger.error("Unable to get list of publications from event with id '{}'", eventId, e);
2154 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
2155 }
2156 }
2157
2158
2159 private JsonObject getPublication(String eventId, String publicationId, Boolean withSignedUrls,
2160 ApiVersion requestedVersion)
2161 throws SearchIndexException, NotFoundException {
2162 Optional<Event> eventOpt = indexService.getEvent(eventId, elasticsearchIndex);
2163 if (eventOpt.isPresent()) {
2164 List<Publication> publications;
2165 publications = eventOpt.get().getPublications().stream()
2166 .filter(publication -> (!requestedVersion.isSmallerThan(VERSION_1_11_0)
2167 || EventUtils.internalChannelFilter.test(publication)))
2168 .collect(Collectors.toList());
2169 for (Publication publication : publications) {
2170 if (publicationId.equals(publication.getIdentifier())) {
2171 return getPublication(publication, withSignedUrls, requestedVersion);
2172 }
2173 }
2174 throw new NotFoundException(
2175 String.format("Unable to find publication with id '%s' in event with id '%s'", publicationId, eventId));
2176 }
2177 throw new NotFoundException(String.format("Unable to find event with id '%s'", eventId));
2178 }
2179
2180
2181
2182
2183
2184
2185
2186
2187 protected static AccessControlList getAclFromEvent(Event event) {
2188 AccessControlList activeAcl = new AccessControlList();
2189 try {
2190 if (event.getAccessPolicy() != null) {
2191 activeAcl = AccessControlParser.parseAcl(event.getAccessPolicy());
2192 }
2193 } catch (Exception e) {
2194 logger.error("Unable to parse access policy", e);
2195 }
2196 return activeAcl;
2197 }
2198
2199 private JsonObject getJsonStream(Stream stream) {
2200 JsonObject json = new JsonObject();
2201
2202 if (stream instanceof AudioStream) {
2203 AudioStream audio = (AudioStream) stream;
2204
2205 if (audio.getBitDepth() != null) {
2206 json.addProperty("bitdepth", audio.getBitDepth());
2207 }
2208 if (audio.getBitRate() != null) {
2209 json.addProperty("bitrate", audio.getBitRate());
2210 }
2211 if (audio.getCaptureDevice() != null) {
2212 json.addProperty("capturedevice", audio.getCaptureDevice());
2213 }
2214 if (audio.getCaptureDeviceVendor() != null) {
2215 json.addProperty("capturedevicevendor", audio.getCaptureDeviceVendor());
2216 }
2217 if (audio.getCaptureDeviceVersion() != null) {
2218 json.addProperty("capturedeviceversion", audio.getCaptureDeviceVersion());
2219 }
2220 if (audio.getChannels() != null) {
2221 json.addProperty("channels", audio.getChannels());
2222 }
2223 if (audio.getEncoderLibraryVendor() != null) {
2224 json.addProperty("encoderlibraryvendor", audio.getEncoderLibraryVendor());
2225 }
2226 if (audio.getFormat() != null) {
2227 json.addProperty("format", audio.getFormat());
2228 }
2229 if (audio.getFormatVersion() != null) {
2230 json.addProperty("formatversion", audio.getFormatVersion());
2231 }
2232 if (audio.getFrameCount() != null) {
2233 json.addProperty("framecount", audio.getFrameCount());
2234 }
2235 if (audio.getIdentifier() != null) {
2236 json.addProperty("identifier", audio.getIdentifier());
2237 }
2238 if (audio.getPkLevDb() != null) {
2239 json.addProperty("pklevdb", audio.getPkLevDb());
2240 }
2241 if (audio.getRmsLevDb() != null) {
2242 json.addProperty("rmslevdb", audio.getRmsLevDb());
2243 }
2244 if (audio.getRmsPkDb() != null) {
2245 json.addProperty("rmspkdb", audio.getRmsPkDb());
2246 }
2247 if (audio.getSamplingRate() != null) {
2248 json.addProperty("samplingrate", audio.getSamplingRate());
2249 }
2250
2251 } else if (stream instanceof VideoStream) {
2252 VideoStream video = (VideoStream) stream;
2253
2254 if (video.getBitRate() != null) {
2255 json.addProperty("bitrate", video.getBitRate());
2256 }
2257 if (video.getCaptureDevice() != null) {
2258 json.addProperty("capturedevice", video.getCaptureDevice());
2259 }
2260 if (video.getCaptureDeviceVendor() != null) {
2261 json.addProperty("capturedevicevendor", video.getCaptureDeviceVendor());
2262 }
2263 if (video.getCaptureDeviceVersion() != null) {
2264 json.addProperty("capturedeviceversion", video.getCaptureDeviceVersion());
2265 }
2266 if (video.getEncoderLibraryVendor() != null) {
2267 json.addProperty("encoderlibraryvendor", video.getEncoderLibraryVendor());
2268 }
2269 if (video.getFormat() != null) {
2270 json.addProperty("format", video.getFormat());
2271 }
2272 if (video.getFormatVersion() != null) {
2273 json.addProperty("formatversion", video.getFormatVersion());
2274 }
2275 if (video.getFrameCount() != null) {
2276 json.addProperty("framecount", video.getFrameCount());
2277 }
2278 if (video.getFrameHeight() != null) {
2279 json.addProperty("frameheight", video.getFrameHeight());
2280 }
2281 if (video.getFrameRate() != null) {
2282 json.addProperty("framerate", video.getFrameRate());
2283 }
2284 if (video.getFrameWidth() != null) {
2285 json.addProperty("framewidth", video.getFrameWidth());
2286 }
2287 if (video.getIdentifier() != null) {
2288 json.addProperty("identifier", video.getIdentifier());
2289 }
2290 if (video.getScanOrder() != null) {
2291 json.addProperty("scanorder", video.getScanOrder().toString());
2292 }
2293 if (video.getScanType() != null) {
2294 json.addProperty("scantype", video.getScanType().toString());
2295 }
2296 }
2297
2298 return json;
2299 }
2300
2301 private String getEventUrl(String eventId) {
2302 return UrlSupport.concat(endpointBaseUrl, eventId);
2303 }
2304
2305 @GET
2306 @Path("{eventId}/scheduling")
2307 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
2308 ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
2309 ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
2310 ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
2311 @RestQuery(
2312 name = "geteventscheduling",
2313 description = "Returns an event's scheduling information.",
2314 returnDescription = "",
2315 pathParameters = {
2316 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
2317 },
2318 responses = {
2319 @RestResponse(description = "The scheduling information for the specified event is returned.",
2320 responseCode = HttpServletResponse.SC_OK),
2321 @RestResponse(description = "The specified event has no scheduling information.",
2322 responseCode = HttpServletResponse.SC_NO_CONTENT),
2323 @RestResponse(description = "The specified event does not exist.",
2324 responseCode = HttpServletResponse.SC_NOT_FOUND)
2325 })
2326 public Response getEventScheduling(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
2327 throws Exception {
2328 try {
2329 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
2330
2331 if (event.isEmpty()) {
2332 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
2333 }
2334
2335 final JsonObject scheduling = SchedulingInfo.of(event.get().getIdentifier(), schedulerService).toJson();
2336 if (!scheduling.isEmpty()) {
2337 return ApiResponseBuilder.Json.ok(acceptHeader, scheduling);
2338 }
2339 return Response.noContent().build();
2340 } catch (SearchIndexException e) {
2341 logger.error("Unable to get list of publications from event with id '{}'", id, e);
2342 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
2343 }
2344 }
2345
2346 @PUT
2347 @Path("{eventId}/scheduling")
2348 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
2349 ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
2350 ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
2351 ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
2352 @RestQuery(
2353 name = "updateeventscheduling",
2354 description = "Update an event's scheduling information.",
2355 returnDescription = "",
2356 pathParameters = {
2357 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = Type.STRING)
2358 },
2359 restParameters = {
2360 @RestParameter(name = "scheduling", isRequired = true, description = "Scheduling Information",
2361 type = Type.STRING),
2362 @RestParameter(name = "allowConflict", description = "Allow conflicts when updating scheduling",
2363 isRequired = false, type = Type.BOOLEAN)
2364 },
2365 responses = {
2366 @RestResponse(description = "The scheduling information for the specified event is updated.",
2367 responseCode = HttpServletResponse.SC_NO_CONTENT),
2368 @RestResponse(description = "The specified event has no scheduling information to update.",
2369 responseCode = HttpServletResponse.SC_NOT_ACCEPTABLE),
2370 @RestResponse(description = "The scheduling information could not be updated due to a conflict.",
2371 responseCode = HttpServletResponse.SC_CONFLICT),
2372 @RestResponse(description = "The specified event does not exist.",
2373 responseCode = HttpServletResponse.SC_NOT_FOUND)
2374 })
2375 public Response updateEventScheduling(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
2376 @FormParam("scheduling") String scheduling,
2377 @FormParam("allowConflict") @DefaultValue("false") boolean allowConflict) throws Exception {
2378 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
2379 final Optional<Event> event = indexService.getEvent(id, elasticsearchIndex);
2380
2381 if (requestedVersion.isSmallerThan(ApiVersion.VERSION_1_2_0)) {
2382 allowConflict = false;
2383 }
2384 if (event.isEmpty()) {
2385 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
2386 }
2387 final JSONParser parser = new JSONParser();
2388 JSONObject parsedJson;
2389 try {
2390 parsedJson = (JSONObject) parser.parse(scheduling);
2391 } catch (ParseException e) {
2392 logger.debug("Client sent unparsable scheduling information for event {}: {}", id, scheduling);
2393 return RestUtil.R.badRequest("Unparsable scheduling information");
2394 }
2395 Optional<Response> clientError = updateSchedulingInformation(parsedJson, id, requestedVersion, allowConflict);
2396 return clientError.orElse(Response.noContent().build());
2397 }
2398
2399 private Optional<Response> updateSchedulingInformation(
2400 JSONObject parsedScheduling,
2401 String id,
2402 ApiVersion requestedVersion,
2403 boolean allowConflict) throws Exception {
2404
2405 SchedulingInfo schedulingInfo;
2406 try {
2407 schedulingInfo = SchedulingInfo.of(parsedScheduling);
2408 } catch (DateTimeParseException e) {
2409 logger.debug("Client sent unparsable start or end date for event {}", id);
2410 return Optional.of(RestUtil.R.badRequest("Unparsable date in scheduling information"));
2411 }
2412 final TechnicalMetadata technicalMetadata = schedulerService.getTechnicalMetadata(id);
2413
2414
2415 Optional<Map<String, String>> caConfig = Optional.empty();
2416 if (schedulingInfo.getInputs().isPresent()) {
2417 final Map<String, String> configMap = new HashMap<>(technicalMetadata.getCaptureAgentConfiguration());
2418 configMap.put(CaptureParameters.CAPTURE_DEVICE_NAMES, schedulingInfo.getInputs().get());
2419 caConfig = Optional.of(configMap);
2420 }
2421
2422 try {
2423 schedulerService.updateEvent(
2424 id,
2425 schedulingInfo.getStartDate(),
2426 schedulingInfo.getEndDate(),
2427 schedulingInfo.getAgentId(),
2428 Optional.empty(),
2429 Optional.empty(),
2430 Optional.empty(),
2431 caConfig,
2432 allowConflict);
2433 } catch (SchedulerConflictException e) {
2434 final List<MediaPackage> conflictingEvents = getConflictingEvents(
2435 schedulingInfo.merge(technicalMetadata), agentStateService, schedulerService);
2436 logger.debug("Client tried to change scheduling information causing a conflict for event {}.", id);
2437 List<JsonObject> conflicts = convertConflictingEvents(
2438 Optional.of(id), conflictingEvents, indexService, elasticsearchIndex
2439 );
2440
2441 JsonArray conflictArray = new JsonArray();
2442 for (JsonObject conflict : conflicts) {
2443 conflictArray.add(conflict);
2444 }
2445
2446 return Optional.of(ApiResponseBuilder.Json.conflict(requestedVersion, conflictArray));
2447 }
2448 return Optional.empty();
2449 }
2450
2451 @POST
2452 @Path("{eventId}/track")
2453 @Consumes(MediaType.MULTIPART_FORM_DATA)
2454 @RestQuery(
2455 name = "updateFlavorWithTrack",
2456 description = "Update an events track for a given flavor",
2457 returnDescription = "",
2458 pathParameters = {
2459 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) },
2460 restParameters = {
2461 @RestParameter(description = "Flavor to add track to, e.g. captions/source",
2462 isRequired = true, name = "flavor", type = RestParameter.Type.STRING),
2463 @RestParameter(description = "Comma separated list of tags for the given track, e.g. archive,publish. "
2464 + "If a 'lang:LANG-CODE' tag exists and overwriteExisting=true "
2465 + "only tracks with same lang tag and flavor will be replaced. This behavior is used for captions.",
2466 isRequired = false, name = "tags", type = RestParameter.Type.STRING),
2467 @RestParameter(description = "If true, all other tracks in the specified flavor are REMOVED. "
2468 + "If tags argument contains a lang:LANG-CODE tag, only elements with same tag would be removed.",
2469 isRequired = true, name = "overwriteExisting", type = RestParameter.Type.BOOLEAN),
2470 @RestParameter(description = "The track file", isRequired = true, name = "track",
2471 type = RestParameter.Type.FILE),
2472 },
2473 responses = {
2474 @RestResponse(description = "The specified event does not exist.",
2475 responseCode = HttpServletResponse.SC_NOT_FOUND),
2476 @RestResponse(description = "The track has been added to the event.",
2477 responseCode = HttpServletResponse.SC_OK),
2478 @RestResponse(description = "The request is invalid or inconsistent.",
2479 responseCode = HttpServletResponse.SC_BAD_REQUEST),
2480 })
2481 public Response updateFlavorWithTrack(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
2482 @Context HttpServletRequest request) {
2483 logger.debug("updateFlavorWithTrack called");
2484 try {
2485 boolean overwriteExisting = false;
2486 MediaPackageElementFlavor tmpFlavor = MediaPackageElementFlavor.parseFlavor("addTrack/temporary");
2487 MediaPackageElementFlavor newFlavor = null;
2488 Optional<Event> event;
2489 List<String> tags = null;
2490 String langTag = null;
2491
2492 try {
2493 event = indexService.getEvent(id, elasticsearchIndex);
2494 } catch (SearchIndexException e) {
2495 return RestUtil.R.badRequest(String.format("Error while searching for event with id %s; %s",
2496 id, e.getMessage()));
2497 }
2498
2499 if (event.isEmpty()) {
2500 return ApiResponseBuilder.notFound(String.format("Unable to find event with id '%s'", id));
2501 }
2502 MediaPackage mp = indexService.getEventMediapackage(event.get());
2503
2504 try {
2505 if (workflowService.mediaPackageHasActiveWorkflows(mp.getIdentifier().toString())) {
2506 return RestUtil.R.conflict(String.format("Cannot update while a workflow is running on event '%s'", id));
2507 }
2508 } catch (WorkflowDatabaseException e) {
2509 return RestUtil.R.serverError();
2510 }
2511
2512 if (!ServletFileUpload.isMultipartContent(request)) {
2513 throw new IllegalArgumentException("No multipart content");
2514 }
2515 for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
2516 FileItemStream item = iter.next();
2517 String fieldName = item.getFieldName();
2518 if (item.isFormField()) {
2519 if ("flavor".equals(fieldName)) {
2520 String flavorString = Streams.asString(item.openStream());
2521 try {
2522 newFlavor = MediaPackageElementFlavor.parseFlavor(flavorString);
2523 } catch (IllegalArgumentException e) {
2524 return RestUtil.R.badRequest(String.format("Could not parse flavor %s; %s",
2525 flavorString, e.getMessage()));
2526 }
2527 } else if ("tags".equals(fieldName)) {
2528 String tagsString = Streams.asString(item.openStream());
2529 if (StringUtils.isNotBlank(tagsString)) {
2530 tags = List.of(StringUtils.split(tagsString, ','));
2531
2532 for (String tag : tags) {
2533 if (StringUtils.startsWith(StringUtils.trimToEmpty(tag), "lang:")) {
2534
2535 langTag = StringUtils.trimToEmpty(tag);
2536 break;
2537 }
2538 }
2539 }
2540 } else if ("overwriteExisting".equals(fieldName)) {
2541 overwriteExisting = Boolean.parseBoolean(Streams.asString(item.openStream()));
2542 }
2543 } else {
2544
2545 if ("track".equals(item.getFieldName())) {
2546 mp = ingestService.addTrack(item.openStream(), item.getName(), tmpFlavor, mp);
2547 }
2548 }
2549 }
2550
2551 if (overwriteExisting) {
2552
2553 Track[] existing = mp.getTracks(newFlavor);
2554 for (int i = 0; i < existing.length; i++) {
2555
2556 if (null == langTag || existing[i].containsTag(langTag)) {
2557 mp.remove(existing[i]);
2558 logger.debug("Overwriting existing asset {} {}", tmpFlavor, newFlavor);
2559 }
2560 }
2561 }
2562
2563 for (Track track : mp.getTracks(tmpFlavor)) {
2564 track.setFlavor(newFlavor);
2565 if (null != tags) {
2566 for (String tag : tags) {
2567 track.addTag(tag);
2568 }
2569 }
2570 }
2571 logger.debug("Updated asset {} {}", tmpFlavor, newFlavor);
2572
2573 try {
2574 assetManager.takeSnapshot(mp);
2575 } catch (AssetManagerException e) {
2576 logger.error("Error while adding the updated media package ({}) to the archive", mp.getIdentifier(), e);
2577 return RestUtil.R.badRequest(e.getMessage());
2578 }
2579
2580 return Response.status(Status.OK).build();
2581 } catch (IllegalArgumentException | IOException | FileUploadException | IndexServiceException | IngestException
2582 | MediaPackageException e) {
2583 return RestUtil.R.badRequest(String.format("Could not add track: %s", e.getMessage()));
2584 }
2585 }
2586 }