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 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
113 private static final Logger logger = LoggerFactory.getLogger(ListProviderEndpoint.class);
114
115
116 protected String endpointBaseUrl;
117
118
119 private PlaylistService service;
120
121
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
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
254 Playlist playlist = restService.parseJsonToPlaylist(playlistText);
255
256
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 }