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