1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
108 private static final Logger logger = LoggerFactory.getLogger(ListProviderEndpoint.class);
109
110
111 protected String endpointBaseUrl;
112
113
114 private PlaylistService service;
115
116
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
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
251 Playlist playlist = restService.parseJsonToPlaylist(playlistText);
252
253
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 }