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.", responseCode = HttpServletResponse.SC_NOT_FOUND),
152           @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
153           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
154       })
155   public Response getPlaylistAsJson(
156       @HeaderParam("Accept") String acceptHeader,
157       @PathParam("id") String id) {
158     try {
159       Playlist playlist = service.getPlaylistById(id);
160 
161       return ApiResponseBuilder.Json.ok(acceptHeader, playlistToJson(playlist));
162     } catch (NotFoundException e) {
163       return ApiResponseBuilder.notFound("Cannot find playlist instance with id '%s'.", id);
164     } catch (UnauthorizedException e) {
165       return Response.status(Response.Status.FORBIDDEN).build();
166     } catch (IllegalStateException e) {
167       return Response.status(Response.Status.BAD_REQUEST).build();
168     }
169   }
170 
171   @GET
172   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
173   @Path("")
174   @RestQuery(
175       name = "playlists",
176       description = "Get playlists. Playlists that you do not have read access to will not show up.",
177       returnDescription = "A JSON object containing an array.",
178       restParameters = {
179           @RestParameter(name = "limit", isRequired = false, type = INTEGER,
180               description = "The maximum number of results to return for a single request.", defaultValue = "100"),
181           @RestParameter(name = "offset", isRequired = false, type = INTEGER,
182               description = "The index of the first result to return."),
183           @RestParameter(name = "sort", isRequired = false, type = STRING,
184               description = "Sort the results based upon a sorting criteria. A criteria is specified as a pair such as:"
185                   + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or"
186                   + "descending order and is mandatory. Sort Name is case sensitive. Supported Sort Names are 'updated'"
187               , defaultValue = "updated:ASC"),
188       },
189       responses = {
190           @RestResponse(description = "Returns the playlist.", responseCode = HttpServletResponse.SC_OK),
191           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
192       })
193   public Response getPlaylistsAsJson(
194       @HeaderParam("Accept") String acceptHeader,
195       @QueryParam("limit") int limit,
196       @QueryParam("offset") int offset,
197       @QueryParam("sort") String sort) {
198     if (offset < 0) {
199       return Response.status(Response.Status.BAD_REQUEST).build();
200     }
201 
202     if (limit < 0) {
203       return Response.status(Response.Status.BAD_REQUEST).build();
204     }
205 
206     SortCriterion sortCriterion = new SortCriterion("", SortCriterion.Order.None);
207     Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
208     if (optSort.isPresent()) {
209       sortCriterion = SortCriterion.parse(optSort.get());
210 
211       switch (sortCriterion.getFieldName()) {
212         case "updated":
213           break;
214         default:
215           logger.info("Unknown sort criteria {}", sortCriterion.getFieldName());
216           return Response.serverError().status(Response.Status.BAD_REQUEST).build();
217       }
218     }
219 
220     List<Playlist> playlists = service.getPlaylists(limit, offset, sortCriterion);
221 
222     JsonArray playlistsJson = new JsonArray();
223     for (Playlist p : playlists) {
224       playlistsJson.add(playlistToJson(p));
225     }
226 
227     return Response.ok(playlistsJson.toString(), acceptHeader).build();
228   }
229 
230   @POST
231   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
232   @Path("")
233   @RestQuery(
234       name = "create",
235       description = "Creates a playlist.",
236       returnDescription = "The created playlist.",
237       restParameters = {
238           @RestParameter(name = "playlist", isRequired = false, description = "Playlist in JSON format", type = TEXT,
239               jaxbClass = JaxbPlaylist.class, defaultValue = SAMPLE_PLAYLIST_JSON)
240       },
241       responses = {
242           @RestResponse(description = "Playlist created.", responseCode = HttpServletResponse.SC_CREATED),
243           @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
244           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
245       })
246   public Response createAsJson(
247       @HeaderParam("Accept") String acceptHeader,
248       @FormParam("playlist") String playlistText) {
249     try {
250       // Map JSON to JPA
251       Playlist playlist = restService.parseJsonToPlaylist(playlistText);
252 
253       // Persist
254       playlist = service.update(playlist);
255       return ApiResponseBuilder.Json.created(
256           acceptHeader,
257           URI.create(getPlaylistUrl(playlist.getId())),
258           playlistToJson(playlist)
259       );
260     } catch (UnauthorizedException e) {
261       return Response.status(Response.Status.FORBIDDEN).build();
262     } catch (ParseException | IOException | IllegalArgumentException e) {
263       return Response.status(Response.Status.BAD_REQUEST).build();
264     }
265   }
266 
267   @PUT
268   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
269   @Path("{id}")
270   @RestQuery(
271       name = "update",
272       description = "Updates a playlist.",
273       returnDescription = "The updated playlist.",
274       pathParameters = {
275           @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING)
276       },
277       restParameters = {
278           @RestParameter(name = "playlist", isRequired = false, description = "Playlist in JSON format", type = TEXT,
279               jaxbClass = JaxbPlaylist.class, defaultValue = SAMPLE_PLAYLIST_JSON)
280       },
281       responses = {
282           @RestResponse(description = "Playlist updated.", responseCode = HttpServletResponse.SC_OK),
283           @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
284           @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
285       })
286   public Response updateAsJson(
287       @HeaderParam("Accept") String acceptHeader,
288       @PathParam("id") String id,
289       @FormParam("playlist") String playlistText) {
290     try {
291       Playlist playlist = service.updateWithJson(id, playlistText);
292       return ApiResponseBuilder.Json.ok(acceptHeader, playlistToJson(playlist));
293     } catch (UnauthorizedException e) {
294       return Response.status(Response.Status.FORBIDDEN).build();
295     } catch (IOException | IllegalArgumentException e) {
296       return Response.status(Response.Status.BAD_REQUEST).build();
297     }
298   }
299 
300   @DELETE
301   @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_11_0 })
302   @Path("{id}")
303   @RestQuery(
304       name = "remove",
305       description = "Removes a playlist.",
306       returnDescription = "The removed playlist.",
307       pathParameters = {
308           @RestParameter(name = "id", isRequired = true, description = "Playlist identifier", type = STRING)
309       },
310       responses = {
311           @RestResponse(responseCode = SC_OK, description = "Playlist removed."),
312           @RestResponse(responseCode = SC_NOT_FOUND, description = "No playlist with that identifier exists."),
313           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action")
314       })
315   public Response remove(
316       @HeaderParam("Accept") String acceptHeader,
317       @PathParam("id") String id) {
318     try {
319       Playlist playlist = service.remove(id);
320 
321       return ApiResponseBuilder.Json.ok(acceptHeader, playlistToJson(playlist));
322     } catch (NotFoundException e) {
323       return ApiResponseBuilder.notFound("Cannot find playlist instance with id '%s'.", id);
324     } catch (UnauthorizedException e) {
325       return Response.status(Response.Status.FORBIDDEN).build();
326     }
327   }
328 
329   private JsonObject playlistToJson(Playlist playlist) {
330     JsonObject json = new JsonObject();
331 
332     json.addProperty("id", playlist.getId());
333     JsonArray entriesArray = new JsonArray();
334     for (PlaylistEntry entry : playlist.getEntries()) {
335       entriesArray.add(playlistEntryToJson(entry));
336     }
337     json.add("entries", entriesArray);
338     json.addProperty("title", safeString(playlist.getTitle()));
339     json.addProperty("description", safeString(playlist.getDescription()));
340     json.addProperty("creator", safeString(playlist.getCreator()));
341     json.addProperty("updated", playlist.getUpdated() != null ? toUTC(playlist.getUpdated().getTime()) : "");
342     JsonArray aceArray = new JsonArray();
343     for (PlaylistAccessControlEntry ace : playlist.getAccessControlEntries()) {
344       aceArray.add(playlistAccessControlEntryToJson(ace));
345     }
346     json.add("accessControlEntries", aceArray);
347 
348     return json;
349   }
350 
351   private JsonObject playlistEntryToJson(PlaylistEntry playlistEntry) {
352     JsonObject json = new JsonObject();
353 
354     json.addProperty("id", playlistEntry.getId());
355     if (playlistEntry.getContentId() != null) {
356       json.addProperty("contentId", playlistEntry.getContentId());
357     } else {
358       json.add("contentId", null);
359     }
360 
361     json.add("type", enumToJSON(playlistEntry.getType()));
362 
363     return json;
364   }
365 
366   private JsonObject playlistAccessControlEntryToJson(PlaylistAccessControlEntry playlistAccessControlEntry) {
367     JsonObject json = new JsonObject();
368 
369     json.addProperty("id", playlistAccessControlEntry.getId());
370     json.addProperty("allow", playlistAccessControlEntry.isAllow());
371     json.addProperty("role", playlistAccessControlEntry.getRole());
372     json.addProperty("action", playlistAccessControlEntry.getAction());
373 
374     return json;
375   }
376 
377   private JsonElement enumToJSON(Enum<?> e) {
378     return e == null ? null : new JsonPrimitive(e.toString());
379   }
380 
381   private String getPlaylistUrl(String playlistId) {
382     return UrlSupport.concat(endpointBaseUrl, playlistId);
383   }
384 }