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