View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   * A REST endpoint for the {@link PlaylistService}
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   /** The logger */
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   /** The playlist service instance */
162   private PlaylistService service;
163 
164   /** The search service */
165   protected SearchService searchService = null;
166 
167   /** The authorization service */
168   protected AuthorizationService authorizationService = null;
169 
170   /**
171    * Sets the playlist service
172    *
173    * @param service
174    *          the playlist service instance
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       // Map JSON to JPA
398       Playlist playlist = parseJsonToPlaylist(playlistText);
399 
400       // Persist
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       // Map XML to JPA
433       Playlist playlist = parseXmlToPlaylist(playlistText);
434 
435       // Persist
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       // Persist
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    * While jackson takes care of automatically converting JAXB to JSON when returning from a request, getting it to
637    * parse JSON to JAXB when accepting a request is not that automatic. This functions takes care of that.
638    * @param json Valid JSON as a string
639    * @return A Playlist containing the information from the JSON
640    * @throws ParseException
641    * @throws IOException
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 }