1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.playlists;
22
23 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
24 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
25 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
26 import static javax.servlet.http.HttpServletResponse.SC_OK;
27 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
28 import static org.apache.commons.lang3.StringUtils.trimToNull;
29 import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
30 import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
31 import static org.opencastproject.util.doc.rest.RestParameter.Type.LONG;
32 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
33 import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
34
35 import org.opencastproject.playlists.serialization.JaxbPlaylist;
36 import org.opencastproject.search.api.SearchService;
37 import org.opencastproject.security.api.AuthorizationService;
38 import org.opencastproject.security.api.UnauthorizedException;
39 import org.opencastproject.util.NotFoundException;
40 import org.opencastproject.util.XmlSafeParser;
41 import org.opencastproject.util.data.Option;
42 import org.opencastproject.util.doc.rest.RestParameter;
43 import org.opencastproject.util.doc.rest.RestQuery;
44 import org.opencastproject.util.doc.rest.RestResponse;
45 import org.opencastproject.util.doc.rest.RestService;
46 import org.opencastproject.util.requests.SortCriterion;
47
48 import com.fasterxml.jackson.databind.DeserializationFeature;
49 import com.fasterxml.jackson.databind.JsonNode;
50 import com.fasterxml.jackson.databind.ObjectMapper;
51 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
52 import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
53
54 import org.apache.commons.io.IOUtils;
55 import org.json.simple.parser.ParseException;
56 import org.osgi.service.component.annotations.Component;
57 import org.osgi.service.component.annotations.Reference;
58 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.xml.sax.SAXException;
62
63 import java.io.IOException;
64 import java.util.ArrayList;
65 import java.util.List;
66
67 import javax.ws.rs.DELETE;
68 import javax.ws.rs.FormParam;
69 import javax.ws.rs.GET;
70 import javax.ws.rs.POST;
71 import javax.ws.rs.PUT;
72 import javax.ws.rs.Path;
73 import javax.ws.rs.PathParam;
74 import javax.ws.rs.Produces;
75 import javax.ws.rs.core.GenericEntity;
76 import javax.ws.rs.core.MediaType;
77 import javax.ws.rs.core.Response;
78 import javax.xml.bind.JAXBContext;
79 import javax.xml.bind.JAXBException;
80
81
82
83
84 @Path("/playlists")
85 @RestService(
86 name = "playlistservice",
87 title = "Playlist Service",
88 abstractText = "This service lists available playlists and stuff",
89 notes = {
90 "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
91 "If the service is down or not working it will return a status 503, this means the the underlying service is "
92 + "not working and is either restarting or has failed",
93 "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
94 + "other words, there is a bug! You should file an error report with your server logs from the time when the "
95 + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
96 @Component(
97 immediate = true,
98 service = PlaylistRestService.class,
99 property = {
100 "service.description=Playlist REST Endpoint",
101 "opencast.service.type=org.opencastproject.playlists",
102 "opencast.service.path=/playlists"
103 }
104 )
105 @JaxrsResource
106 public class PlaylistRestService {
107
108 private static final Logger logger = LoggerFactory.getLogger(PlaylistRestService.class);
109
110 public static final String SAMPLE_PLAYLIST_JSON = "{\n"
111 + " \"title\": \"Opencast Playlist\",\n"
112 + " \"description\": \"This is a playlist about Opencast\",\n"
113 + " \"creator\": \"Opencast\",\n"
114 + " \"entries\": [\n"
115 + " {\n"
116 + " \"contentId\": \"ID-about-opencast\",\n"
117 + " \"type\": \"EVENT\"\n"
118 + " },\n"
119 + " {\n"
120 + " \"contentId\": \"ID-3d-print\",\n"
121 + " \"type\": \"EVENT\"\n"
122 + " }\n"
123 + " ],\n"
124 + " \"accessControlEntries\": [\n"
125 + " {\n"
126 + " \"allow\": true,\n"
127 + " \"role\": \"ROLE_USER_BOB\",\n"
128 + " \"action\": \"read\"\n"
129 + " }\n"
130 + " ]\n"
131 + "}";
132
133 public static final String SAMPLE_PLAYLIST_ENTRIES_JSON = "[\n"
134 + " {\n"
135 + " \"contentId\": \"ID-about-opencast\",\n"
136 + " \"type\": \"EVENT\"\n"
137 + " },\n"
138 + " {\n"
139 + " \"contentId\": \"ID-3d-print\",\n"
140 + " \"type\": \"EVENT\"\n"
141 + " }\n"
142 + " ],";
143
144 public static final String SAMPLE_PLAYLIST_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><"
145 + "ns3:playlist xmlns:ns2=\"http://mediapackage.opencastproject.org\" "
146 + "xmlns:ns3=\"http://playlist.opencastproject.org\"><organization>mh_default_org</organization>"
147 + "<entries><contentId>ID-av-portal</contentId><type>EVENT</type></entries><entries>"
148 + "<contentId>ID-av-print</contentId><type>EVENT</type></entries><title>Opencast Playlist</title>"
149 + "<description>This is a playlist about Opencast</description><creator>Opencast</creator>"
150 + "<updated>1701787700848</updated><accessControlEntries><allow>true</allow><role>ROLE_USER_BOB</role>"
151 + "<action>read</action></accessControlEntries></ns3:playlist>";
152
153 public static final String SAMPLE_PLAYLIST_ENTRIES_XML =
154 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
155 + "<entries>\n"
156 + "\t<entry id=\"1061\">\n" + "\t\t<contentId>ID-av-portal</contentId>\n" + "\t\t<type>EVENT</type>\n"
157 + "\t</entry>\n"
158 + "\t<entry id=\"1062\">\n" + "\t\t<contentId>ID-av-print</contentId>\n" + "\t\t<type>EVENT</type>\n"
159 + "\t</entry>\n" + "</entries>";
160
161
162 private PlaylistService service;
163
164
165 protected SearchService searchService = null;
166
167
168 protected AuthorizationService authorizationService = null;
169
170
171
172
173
174
175
176 @Reference
177 public void setService(PlaylistService service) {
178 this.service = service;
179 }
180
181 @Reference
182 protected void setSearchService(SearchService searchService) {
183 this.searchService = searchService;
184 }
185
186 @Reference
187 public void setAuthorizationService(AuthorizationService authorizationService) {
188 this.authorizationService = authorizationService;
189 }
190
191 @GET
192 @Produces(MediaType.APPLICATION_JSON)
193 @Path("{id}.json")
194 @RestQuery(
195 name = "playlist",
196 description = "Get a playlist.",
197 returnDescription = "A playlist as JSON",
198 pathParameters = {
199 @RestParameter(name = "id", isRequired = true, description = "The playlist identifier", type = STRING),
200 },
201 restParameters = {
202 @RestParameter(name = "withPublications", isRequired = false, description = "If available publications for"
203 + "the content should be returned. Only works for content of type EVENT.", type = BOOLEAN,
204 defaultValue = "true")
205 },
206 responses = {
207 @RestResponse(responseCode = SC_OK, description = "A playlist as JSON."),
208 @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist with that identifier exists."),
209 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
210 })
211 public Response getPlaylistAsJson(
212 @PathParam("id") String id,
213 @FormParam("withPublications") boolean withPublications)
214 throws NotFoundException, UnauthorizedException {
215 Playlist playlist = service.getPlaylistById(id);
216
217 JaxbPlaylist jaxbPlaylist;
218 if (withPublications) {
219 jaxbPlaylist = service.enrich(playlist);
220 } else {
221 jaxbPlaylist = new JaxbPlaylist(playlist);
222 }
223
224 return Response.ok().entity(jaxbPlaylist).build();
225 }
226
227 @GET
228 @Produces(MediaType.APPLICATION_XML)
229 @Path("{id}.xml")
230 @RestQuery(
231 name = "playlist",
232 description = "Get a playlist.",
233 returnDescription = "A playlist as XML",
234 pathParameters = {
235 @RestParameter(name = "id", isRequired = true, description = "The playlist identifier", type = STRING),
236 },
237 restParameters = {
238 @RestParameter(
239 name = "withPublications",
240 isRequired = false,
241 description = "If available publications for"
242 + "the content should be returned. Only works for content of type EVENT.",
243 type = BOOLEAN,
244 defaultValue = "true"
245 )
246 },
247 responses = {
248 @RestResponse(responseCode = SC_OK, description = "A playlist as XML."),
249 @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist with that identifier exists."),
250 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
251 })
252 public Response getPlaylistAsXml(
253 @PathParam("id") String id,
254 @FormParam("withPublications") boolean withPublications)
255 throws NotFoundException, UnauthorizedException {
256 return getPlaylistAsJson(id, withPublications);
257 }
258
259 @GET
260 @Produces(MediaType.APPLICATION_JSON)
261 @Path("playlists.json")
262 @RestQuery(
263 name = "playlists",
264 description = "Get playlists. Playlists that you do not have read access to will not show up.",
265 returnDescription = "A JSON object containing an array.",
266 restParameters = {
267 @RestParameter(
268 name = "limit",
269 isRequired = false,
270 type = INTEGER,
271 description = "The maximum number of results to return for a single request.",
272 defaultValue = "100"
273 ),
274 @RestParameter(
275 name = "offset",
276 isRequired = false,
277 type = INTEGER,
278 description = "The index of the first result to return."
279 ),
280 @RestParameter(
281 name = "sort",
282 isRequired = false,
283 type = STRING,
284 description = "Sort the results based upon a sorting criteria. A criteria is specified as a pair such as:"
285 + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or"
286 + "descending order and is mandatory. Sort Name is case sensitive. Supported Sort Names are 'updated'"
287 ,
288 defaultValue = "updated:ASC"
289 ),
290 },
291 responses = {
292 @RestResponse(responseCode = SC_OK, description = "A playlist as JSON."),
293 @RestResponse(responseCode = SC_BAD_REQUEST, description = "A request parameter was illegal."),
294 })
295 public Response getPlaylistsAsJson(
296 @FormParam("limit") int limit,
297 @FormParam("offset") int offset,
298 @FormParam("sort") String sort)
299 throws NotFoundException {
300 if (offset < 0) {
301 return Response.status(SC_BAD_REQUEST).build();
302 }
303
304 if (limit < 0) {
305 return Response.status(SC_BAD_REQUEST).build();
306 }
307
308 SortCriterion sortCriterion = new SortCriterion("", SortCriterion.Order.None);
309 Option<String> optSort = Option.option(trimToNull(sort));
310 if (optSort.isSome()) {
311 sortCriterion = SortCriterion.parse(optSort.get());
312
313 switch (sortCriterion.getFieldName()) {
314 case "updated":
315 break;
316 default:
317 logger.info("Unknown sort criteria {}", sortCriterion.getFieldName());
318 return Response.status(SC_BAD_REQUEST).build();
319 }
320 }
321
322 List<JaxbPlaylist> jaxbPlaylists = new ArrayList<>();
323 for (Playlist playlist : service.getPlaylists(limit, offset, sortCriterion)) {
324 jaxbPlaylists.add(new JaxbPlaylist(playlist));
325 }
326
327 return Response.ok().entity(new GenericEntity<>(jaxbPlaylists) { }).build();
328 }
329
330 @GET
331 @Produces(MediaType.APPLICATION_XML)
332 @Path("playlists.xml")
333 @RestQuery(
334 name = "playlists",
335 description = "Get playlists. Playlists that you do not have read access to will not show up.",
336 returnDescription = "A XML object containing an array.",
337 restParameters = {
338 @RestParameter(
339 name = "limit",
340 isRequired = false,
341 type = INTEGER,
342 description = "The maximum number of results to return for a single request.",
343 defaultValue = "100"
344 ),
345 @RestParameter(
346 name = "offset",
347 isRequired = false,
348 type = INTEGER,
349 description = "The index of the first result to return."
350 ),
351 @RestParameter(
352 name = "sort",
353 isRequired = false,
354 type = STRING,
355 description = "Sort the results based upon a sorting criteria. A criteria is specified as a pair such as:"
356 + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or"
357 + "descending order and is mandatory. Sort Name is case sensitive. Supported Sort Names are 'updated'"
358 ,
359 defaultValue = "updated:ASC"
360 ),
361 },
362 responses = {
363 @RestResponse(responseCode = SC_OK, description = "A playlist as XML."),
364 })
365 public Response getPlaylistsAsXml(
366 @FormParam("limit") int limit,
367 @FormParam("offset") int offset,
368 @FormParam("sort") String sort)
369 throws NotFoundException {
370 return getPlaylistsAsJson(limit, offset, sort);
371 }
372
373 @POST
374 @Produces(MediaType.APPLICATION_JSON)
375 @Path("new.json")
376 @RestQuery(
377 name = "create",
378 description = "Creates a playlist.",
379 returnDescription = "The created playlist.",
380 restParameters = {
381 @RestParameter(
382 name = "playlist",
383 isRequired = false,
384 description = "Playlist in JSON format",
385 type = TEXT,
386 jaxbClass = JaxbPlaylist.class,
387 defaultValue = SAMPLE_PLAYLIST_JSON
388 )
389 },
390 responses = {
391 @RestResponse(responseCode = SC_CREATED, description = "Playlist created."),
392 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
393 })
394 public Response createAsJson(@FormParam("playlist") String playlistText)
395 throws UnauthorizedException {
396 try {
397
398 Playlist playlist = parseJsonToPlaylist(playlistText);
399
400
401 playlist = service.update(playlist);
402 return Response.status(Response.Status.CREATED).entity(new JaxbPlaylist(playlist)).build();
403 } catch (Exception e) {
404 return Response.serverError().build();
405 }
406 }
407
408 @POST
409 @Produces(MediaType.APPLICATION_XML)
410 @Path("new.xml")
411 @RestQuery(
412 name = "create",
413 description = "Creates a playlist.",
414 returnDescription = "The created playlist.",
415 restParameters = {
416 @RestParameter(
417 name = "playlist",
418 isRequired = false,
419 description = "Playlist in XML format",
420 type = TEXT,
421 jaxbClass = JaxbPlaylist.class,
422 defaultValue = SAMPLE_PLAYLIST_XML
423 )
424 },
425 responses = {
426 @RestResponse(responseCode = SC_OK, description = "Playlist updated."),
427 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
428 })
429 public Response createAsXml(@FormParam("playlist") String playlistText)
430 throws UnauthorizedException {
431 try {
432
433 Playlist playlist = parseXmlToPlaylist(playlistText);
434
435
436 playlist = service.update(playlist);
437 return Response.ok().entity(new JaxbPlaylist(playlist)).build();
438 } catch (Exception e) {
439 return Response.serverError().build();
440 }
441 }
442
443 @PUT
444 @Produces(MediaType.APPLICATION_JSON)
445 @Path("{id}.json")
446 @RestQuery(
447 name = "update",
448 description = "Updates a playlist.",
449 returnDescription = "The updated playlist.",
450 pathParameters = {
451 @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING)
452 },
453 restParameters = {
454 @RestParameter(
455 name = "playlist",
456 isRequired = false,
457 description = "Playlist in JSON format",
458 type = TEXT,
459 jaxbClass = JaxbPlaylist.class,
460 defaultValue = SAMPLE_PLAYLIST_JSON
461 )
462 },
463 responses = {
464 @RestResponse(responseCode = SC_OK, description = "Playlist updated."),
465 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
466 })
467 public Response updateAsJson(
468 @PathParam("id") String id,
469 @FormParam("playlist") String playlistText
470 )
471 throws UnauthorizedException {
472 try {
473 Playlist playlist = service.updateWithJson(id, playlistText);
474 return Response.ok().entity(new JaxbPlaylist(playlist)).build();
475 } catch (Exception e) {
476 return Response.serverError().build();
477 }
478 }
479
480 @PUT
481 @Produces(MediaType.APPLICATION_XML)
482 @Path("{id}.xml")
483 @RestQuery(
484 name = "update",
485 description = "Updates a playlist.",
486 returnDescription = "The updated playlist.",
487 pathParameters = {
488 @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING)
489 },
490 restParameters = {
491 @RestParameter(
492 name = "playlist",
493 isRequired = false,
494 description = "Playlist in XML format",
495 type = TEXT,
496 jaxbClass = JaxbPlaylist.class,
497 defaultValue = SAMPLE_PLAYLIST_XML
498 )
499 },
500 responses = {
501 @RestResponse(responseCode = SC_OK, description = "Playlist updated."),
502 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
503 })
504 public Response updateAsXml(
505 @PathParam("id") String id,
506 @FormParam("playlist") String playlistText
507 )
508 throws UnauthorizedException {
509 try {
510 XmlMapper xmlMapper = new XmlMapper();
511 JsonNode node = xmlMapper.readTree(playlistText.getBytes());
512
513 ObjectMapper jsonMapper = new ObjectMapper();
514 jsonMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
515 String json = jsonMapper.writeValueAsString(node);
516
517 Playlist playlist = service.updateWithJson(id, json);
518 return Response.ok().entity(new JaxbPlaylist(playlist)).build();
519 } catch (Exception e) {
520 return Response.serverError().build();
521 }
522 }
523
524 @DELETE
525 @Produces(MediaType.APPLICATION_JSON)
526 @Path("{id}")
527 @RestQuery(
528 name = "remove",
529 description = "Removes a playlist.",
530 returnDescription = "The removed playlist.",
531 pathParameters = {
532 @RestParameter(
533 name = "id",
534 isRequired = true,
535 description = "Playlist identifier",
536 type = STRING
537 )
538 },
539 responses = {
540 @RestResponse(responseCode = SC_OK, description = "Playlist removed."),
541 @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist with that identifier exists."),
542 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
543 })
544 public Response remove(@PathParam("id") String id) throws NotFoundException, UnauthorizedException {
545 try {
546
547 Playlist playlist = service.remove(id);
548 return Response.ok().entity(new JaxbPlaylist(playlist)).build();
549 } catch (Exception e) {
550 return Response.serverError().build();
551 }
552 }
553
554 @POST
555 @Produces(MediaType.APPLICATION_JSON)
556 @Path("{id}/entries/new")
557 @RestQuery(
558 name = "addEntry",
559 description = "Add entry to playlist.",
560 returnDescription = "The playlist with the new entry.",
561 pathParameters = {
562 @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING),
563 },
564 restParameters = {
565 @RestParameter(
566 name = "contentId",
567 isRequired = false,
568 description = "Content identifier",
569 type = STRING
570 ),
571 @RestParameter(
572 name = "type",
573 isRequired = false,
574 description = "Entry type. Enum. Valid values are EVENT,"
575 + " INACCESSIBLE.",
576 type = STRING
577 ),
578 },
579 responses = {
580 @RestResponse(responseCode = SC_OK, description = "Playlist updated."),
581 @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist with that identifier exists."),
582 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
583 })
584 public Response addEntry(
585 @PathParam("id") String playlistId,
586 @FormParam("contentId") String contentId,
587 @FormParam("type") PlaylistEntryType type)
588 throws NotFoundException, UnauthorizedException {
589 try {
590 Playlist playlist = service.addEntry(playlistId, contentId, type);
591 return Response.ok().entity(new JaxbPlaylist(playlist)).build();
592 } catch (Exception e) {
593 return Response.serverError().build();
594 }
595 }
596
597 @POST
598 @Produces(MediaType.APPLICATION_JSON)
599 @Path("{id}/entries/{entryId}")
600 @RestQuery(
601 name = "removeEntry",
602 description = "Remove entry from playlist.",
603 returnDescription = "Playlist without the entry.",
604 pathParameters = {
605 @RestParameter(
606 name = "id",
607 isRequired = true,
608 type = STRING,
609 description = "Identifier of the playlist to delete from"
610 ),
611 @RestParameter(
612 name = "entryId",
613 isRequired = false,
614 type = LONG,
615 description = "Identifier of the entry that should be deleted"
616 )
617 },
618 responses = {
619 @RestResponse(responseCode = SC_OK, description = "Playlist updated."),
620 @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist or entry with that identifier exists."),
621 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
622 })
623 public Response addEntry(
624 @PathParam("id") String playlistId,
625 @PathParam("entryId") Long entryId)
626 throws NotFoundException, UnauthorizedException {
627 try {
628 Playlist playlist = service.removeEntry(playlistId, entryId);
629 return Response.ok().entity(new JaxbPlaylist(playlist)).build();
630 } catch (Exception e) {
631 return Response.serverError().build();
632 }
633 }
634
635
636
637
638
639
640
641
642
643 public Playlist parseJsonToPlaylist(String json) throws ParseException, IOException {
644 JaxbAnnotationModule module = new JaxbAnnotationModule();
645 ObjectMapper objectMapper = new ObjectMapper();
646 objectMapper.registerModule(module);
647
648 JaxbPlaylist jaxbPlaylist = objectMapper.readValue(json, JaxbPlaylist.class);
649 return jaxbPlaylist.toPlaylist();
650 }
651
652 private Playlist parseXmlToPlaylist(String xml) throws JAXBException, IOException, SAXException {
653 JAXBContext context = JAXBContext.newInstance(JaxbPlaylist.class);
654 JaxbPlaylist jaxbPlaylist = context.createUnmarshaller()
655 .unmarshal(XmlSafeParser.parse(IOUtils.toInputStream(xml, "UTF8")), JaxbPlaylist.class)
656 .getValue();
657 return jaxbPlaylist.toPlaylist();
658 }
659 }