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