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.external.endpoint;
22  
23  import static com.entwinemedia.fn.data.json.Jsons.BLANK;
24  import static com.entwinemedia.fn.data.json.Jsons.NULL;
25  import static com.entwinemedia.fn.data.json.Jsons.arr;
26  import static com.entwinemedia.fn.data.json.Jsons.f;
27  import static com.entwinemedia.fn.data.json.Jsons.obj;
28  import static com.entwinemedia.fn.data.json.Jsons.v;
29  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
30  import static javax.servlet.http.HttpServletResponse.SC_OK;
31  import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
32  import static org.apache.commons.lang3.StringUtils.trimToNull;
33  import static org.opencastproject.playlists.PlaylistRestService.SAMPLE_PLAYLIST_JSON;
34  import static org.opencastproject.util.DateTimeSupport.toUTC;
35  import static org.opencastproject.util.RestUtil.getEndpointUrl;
36  import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
37  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
38  import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
39  
40  import org.opencastproject.external.common.ApiMediaType;
41  import org.opencastproject.external.common.ApiResponseBuilder;
42  import org.opencastproject.playlists.Playlist;
43  import org.opencastproject.playlists.PlaylistAccessControlEntry;
44  import org.opencastproject.playlists.PlaylistEntry;
45  import org.opencastproject.playlists.PlaylistRestService;
46  import org.opencastproject.playlists.PlaylistService;
47  import org.opencastproject.playlists.serialization.JaxbPlaylist;
48  import org.opencastproject.rest.RestConstants;
49  import org.opencastproject.security.api.UnauthorizedException;
50  import org.opencastproject.systems.OpencastConstants;
51  import org.opencastproject.util.NotFoundException;
52  import org.opencastproject.util.UrlSupport;
53  import org.opencastproject.util.data.Option;
54  import org.opencastproject.util.data.Tuple;
55  import org.opencastproject.util.doc.rest.RestParameter;
56  import org.opencastproject.util.doc.rest.RestQuery;
57  import org.opencastproject.util.doc.rest.RestResponse;
58  import org.opencastproject.util.doc.rest.RestService;
59  import org.opencastproject.util.requests.SortCriterion;
60  
61  import com.entwinemedia.fn.data.json.Field;
62  import com.entwinemedia.fn.data.json.JValue;
63  
64  import org.json.simple.parser.ParseException;
65  import org.osgi.service.component.ComponentContext;
66  import org.osgi.service.component.annotations.Activate;
67  import org.osgi.service.component.annotations.Component;
68  import org.osgi.service.component.annotations.Reference;
69  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  import java.io.IOException;
74  import java.net.URI;
75  import java.util.ArrayList;
76  import java.util.List;
77  import java.util.stream.Collectors;
78  
79  import javax.servlet.http.HttpServletResponse;
80  import javax.ws.rs.DELETE;
81  import javax.ws.rs.FormParam;
82  import javax.ws.rs.GET;
83  import javax.ws.rs.HeaderParam;
84  import javax.ws.rs.POST;
85  import javax.ws.rs.PUT;
86  import javax.ws.rs.Path;
87  import javax.ws.rs.PathParam;
88  import javax.ws.rs.Produces;
89  import javax.ws.rs.QueryParam;
90  import javax.ws.rs.core.Response;
91  
92  @Path("/api/playlists")
93  @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
94  @RestService(
95      name = "externalapiplaylists",
96      title = "External API Playlists Service",
97      notes = {},
98      abstractText = "Provides access to playlist structures"
99  )
100 @Component(
101     immediate = true,
102     service = PlaylistsEndpoint.class,
103     property = {
104         "service.description=External API - Playlists Endpoint",
105         "opencast.service.type=org.opencastproject.external.playlists",
106         "opencast.service.path=/api/playlists"
107     }
108 )
109 @JaxrsResource
110 public class PlaylistsEndpoint {
111 
112   /** The logging facility */
113   private static final Logger logger = LoggerFactory.getLogger(ListProviderEndpoint.class);
114 
115   /** Base URL of this endpoint */
116   protected String endpointBaseUrl;
117 
118   /** The capture agent service */
119   private PlaylistService service;
120 
121   /** OSGi DI */
122   @Reference
123   public void setPlaylistService(PlaylistService playlistService) {
124     this.service = playlistService;
125   }
126 
127   private PlaylistRestService restService;
128 
129   @Reference
130   public void setPlaylistRestService(PlaylistRestService playlistRestService) {
131     this.restService = playlistRestService;
132   }
133 
134   /** OSGi activation method */
135   @Activate
136   void activate(ComponentContext cc) {
137     logger.info("Activating External API - Playlists Endpoint");
138 
139     final Tuple<String, String> endpointUrl = getEndpointUrl(cc, OpencastConstants.EXTERNAL_API_URL_ORG_PROPERTY,
140         RestConstants.SERVICE_PATH_PROPERTY);
141     endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
142   }
143 
144   @GET
145   @Path("{id}")
146   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
147   @RestQuery(
148       name = "playlist",
149       description = "Get a playlist.",
150       returnDescription = "A playlist as JSON",
151       pathParameters = {
152           @RestParameter(name = "id", isRequired = true, description = "The playlist identifier", type = STRING),
153       },
154       responses = {
155           @RestResponse(description = "Returns the playlist.", responseCode = HttpServletResponse.SC_OK),
156           @RestResponse(description = "The specified playlist instance does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND),
157           @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
158           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
159       })
160   public Response getPlaylistAsJson(
161       @HeaderParam("Accept") String acceptHeader,
162       @PathParam("id") String id) {
163     try {
164       Playlist playlist = service.getPlaylistById(id);
165 
166       return ApiResponseBuilder.Json.ok(acceptHeader, playlistToJson(playlist));
167     } catch (NotFoundException e) {
168       return ApiResponseBuilder.notFound("Cannot find playlist instance with id '%s'.", id);
169     } catch (UnauthorizedException e) {
170       return Response.status(Response.Status.FORBIDDEN).build();
171     } catch (IllegalStateException e) {
172       return Response.status(Response.Status.BAD_REQUEST).build();
173     }
174   }
175 
176   @GET
177   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
178   @Path("")
179   @RestQuery(
180       name = "playlists",
181       description = "Get playlists. Playlists that you do not have read access to will not show up.",
182       returnDescription = "A JSON object containing an array.",
183       restParameters = {
184           @RestParameter(name = "limit", isRequired = false, type = INTEGER,
185               description = "The maximum number of results to return for a single request.", defaultValue = "100"),
186           @RestParameter(name = "offset", isRequired = false, type = INTEGER,
187               description = "The index of the first result to return."),
188           @RestParameter(name = "sort", isRequired = false, type = STRING,
189               description = "Sort the results based upon a sorting criteria. A criteria is specified as a pair such as:"
190                   + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or"
191                   + "descending order and is mandatory. Sort Name is case sensitive. Supported Sort Names are 'updated'"
192               , defaultValue = "updated:ASC"),
193       },
194       responses = {
195           @RestResponse(description = "Returns the playlist.", responseCode = HttpServletResponse.SC_OK),
196           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
197       })
198   public Response getPlaylistsAsJson(
199       @HeaderParam("Accept") String acceptHeader,
200       @QueryParam("limit") int limit,
201       @QueryParam("offset") int offset,
202       @QueryParam("sort") String sort) {
203     if (offset < 0) {
204       return Response.status(Response.Status.BAD_REQUEST).build();
205     }
206 
207     if (limit < 0) {
208       return Response.status(Response.Status.BAD_REQUEST).build();
209     }
210 
211     SortCriterion sortCriterion = new SortCriterion("", SortCriterion.Order.None);
212     Option<String> optSort = Option.option(trimToNull(sort));
213     if (optSort.isSome()) {
214       sortCriterion = SortCriterion.parse(optSort.get());
215 
216       switch (sortCriterion.getFieldName()) {
217         case "updated":
218           break;
219         default:
220           logger.info("Unknown sort criteria {}", sortCriterion.getFieldName());
221           return Response.serverError().status(Response.Status.BAD_REQUEST).build();
222       }
223     }
224     List<Playlist> playlists = service.getPlaylists(limit, offset, sortCriterion);
225 
226     List<JValue> playlistsJson = playlists.stream()
227         .map(p -> playlistToJson(p))
228         .collect(Collectors.toList());
229 
230     return ApiResponseBuilder.Json.ok(acceptHeader, arr(playlistsJson));
231   }
232 
233   @POST
234   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
235   @Path("")
236   @RestQuery(
237       name = "create",
238       description = "Creates a playlist.",
239       returnDescription = "The created playlist.",
240       restParameters = {
241           @RestParameter(name = "playlist", isRequired = false, description = "Playlist in JSON format", type = TEXT,
242               jaxbClass = JaxbPlaylist.class, defaultValue = SAMPLE_PLAYLIST_JSON)
243       },
244       responses = {
245           @RestResponse(description = "Playlist created.", responseCode = HttpServletResponse.SC_CREATED),
246           @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
247           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
248       })
249   public Response createAsJson(
250       @HeaderParam("Accept") String acceptHeader,
251       @FormParam("playlist") String playlistText) {
252     try {
253       // Map JSON to JPA
254       Playlist playlist = restService.parseJsonToPlaylist(playlistText);
255 
256       // Persist
257       playlist = service.update(playlist);
258       return ApiResponseBuilder.Json.created(
259           acceptHeader,
260           URI.create(getPlaylistUrl(playlist.getId())),
261           playlistToJson(playlist)
262       );
263     } catch (UnauthorizedException e) {
264       return Response.status(Response.Status.FORBIDDEN).build();
265     } catch (ParseException | IOException | IllegalArgumentException e) {
266       return Response.status(Response.Status.BAD_REQUEST).build();
267     }
268   }
269 
270   @PUT
271   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
272   @Path("{id}")
273   @RestQuery(
274       name = "update",
275       description = "Updates a playlist.",
276       returnDescription = "The updated playlist.",
277       pathParameters = {
278           @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING)
279       },
280       restParameters = {
281           @RestParameter(name = "playlist", isRequired = false, description = "Playlist in JSON format", type = TEXT,
282               jaxbClass = JaxbPlaylist.class, defaultValue = SAMPLE_PLAYLIST_JSON)
283       },
284       responses = {
285           @RestResponse(description = "Playlist updated.", responseCode = HttpServletResponse.SC_OK),
286           @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
287           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
288       })
289   public Response updateAsJson(
290       @HeaderParam("Accept") String acceptHeader,
291       @PathParam("id") String id,
292       @FormParam("playlist") String playlistText) {
293     try {
294       Playlist playlist = service.updateWithJson(id, playlistText);
295       return ApiResponseBuilder.Json.ok(acceptHeader, playlistToJson(playlist));
296     } catch (UnauthorizedException e) {
297       return Response.status(Response.Status.FORBIDDEN).build();
298     } catch (IOException | IllegalArgumentException e) {
299       return Response.status(Response.Status.BAD_REQUEST).build();
300     }
301   }
302 
303   @DELETE
304   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
305   @Path("{id}")
306   @RestQuery(
307       name = "remove",
308       description = "Removes a playlist.",
309       returnDescription = "The removed playlist.",
310       pathParameters = {
311           @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING)
312       },
313       responses = {
314           @RestResponse(responseCode = SC_OK, description = "Playlist removed."),
315           @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist with that identifier exists."),
316           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
317       })
318   public Response remove(
319       @HeaderParam("Accept") String acceptHeader,
320       @PathParam("id") String id) {
321     try {
322       Playlist playlist = service.remove(id);
323 
324       return ApiResponseBuilder.Json.ok(acceptHeader, playlistToJson(playlist));
325     } catch (NotFoundException e) {
326       return ApiResponseBuilder.notFound("Cannot find playlist instance with id '%s'.", id);
327     } catch (UnauthorizedException e) {
328       return Response.status(Response.Status.FORBIDDEN).build();
329     }
330   }
331 
332   private JValue playlistToJson(Playlist playlist) {
333     List<Field> fields = new ArrayList<>();
334 
335     fields.add(f("id", v(playlist.getId())));
336     fields.add(f("entries", arr(playlist.getEntries()
337         .stream()
338         .map(this::playlistEntryToJson)
339         .collect(Collectors.toList()))));
340     fields.add(f("title", v(playlist.getTitle(), BLANK)));
341     fields.add(f("description", v(playlist.getDescription(), BLANK)));
342     fields.add(f("creator", v(playlist.getCreator(), BLANK)));
343     fields.add(f("updated", v(playlist.getUpdated() != null ? toUTC(playlist.getUpdated().getTime()) : null, BLANK)));
344     fields.add(f("accessControlEntries", arr(playlist.getAccessControlEntries()
345         .stream()
346         .map(this::playlistAccessControlEntryToJson)
347         .collect(Collectors.toList()))));
348 
349     return obj(fields);
350   }
351 
352   private JValue playlistEntryToJson(PlaylistEntry playlistEntry) {
353     List<Field> fields = new ArrayList<>();
354 
355     fields.add(f("id", v(playlistEntry.getId())));
356     fields.add(f("contentId", v(playlistEntry.getContentId(), NULL)));
357     fields.add(f("type", enumToJSON(playlistEntry.getType())));
358     return obj(fields);
359   }
360 
361   private JValue playlistAccessControlEntryToJson(PlaylistAccessControlEntry playlistAccessControlEntry) {
362     List<Field> fields = new ArrayList<>();
363 
364     fields.add(f("id", v(playlistAccessControlEntry.getId())));
365     fields.add(f("allow", v(playlistAccessControlEntry.isAllow())));
366     fields.add(f("role", v(playlistAccessControlEntry.getRole())));
367     fields.add(f("action", v(playlistAccessControlEntry.getAction())));
368 
369     return obj(fields);
370   }
371 
372   private JValue enumToJSON(Enum e) {
373     return e == null ? null : v(e.toString());
374   }
375 
376   private String getPlaylistUrl(String playlistId) {
377     return UrlSupport.concat(endpointBaseUrl, playlistId);
378   }
379 }