1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.assetmanager.impl.endpoint;
22
23 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
24 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
25 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
26 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
27 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
28 import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
29 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
30 import static javax.servlet.http.HttpServletResponse.SC_OK;
31 import static org.opencastproject.assetmanager.api.AssetManager.DEFAULT_OWNER;
32 import static org.opencastproject.systems.OpencastConstants.WORKFLOW_PROPERTIES_NAMESPACE;
33 import static org.opencastproject.util.RestUtil.R.badRequest;
34 import static org.opencastproject.util.RestUtil.R.forbidden;
35 import static org.opencastproject.util.RestUtil.R.noContent;
36 import static org.opencastproject.util.RestUtil.R.notFound;
37 import static org.opencastproject.util.RestUtil.R.ok;
38 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
39
40 import org.opencastproject.assetmanager.api.AssetManager;
41 import org.opencastproject.assetmanager.api.Property;
42 import org.opencastproject.assetmanager.api.PropertyId;
43 import org.opencastproject.assetmanager.api.Value;
44 import org.opencastproject.mediapackage.MediaPackage;
45 import org.opencastproject.mediapackage.MediaPackageImpl;
46 import org.opencastproject.rest.AbstractJobProducerEndpoint;
47 import org.opencastproject.security.api.UnauthorizedException;
48 import org.opencastproject.util.Checksum;
49 import org.opencastproject.util.ChecksumType;
50 import org.opencastproject.util.NotFoundException;
51 import org.opencastproject.util.doc.rest.RestParameter;
52 import org.opencastproject.util.doc.rest.RestParameter.Type;
53 import org.opencastproject.util.doc.rest.RestQuery;
54 import org.opencastproject.util.doc.rest.RestResponse;
55 import org.opencastproject.util.doc.rest.RestService;
56
57 import com.google.gson.Gson;
58 import com.google.gson.reflect.TypeToken;
59
60 import org.apache.commons.lang3.StringUtils;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 import java.util.HashMap;
65 import java.util.Map;
66 import java.util.Optional;
67
68 import javax.ws.rs.DELETE;
69 import javax.ws.rs.FormParam;
70 import javax.ws.rs.GET;
71 import javax.ws.rs.HeaderParam;
72 import javax.ws.rs.POST;
73 import javax.ws.rs.Path;
74 import javax.ws.rs.PathParam;
75 import javax.ws.rs.Produces;
76 import javax.ws.rs.WebApplicationException;
77 import javax.ws.rs.core.MediaType;
78 import javax.ws.rs.core.Response;
79
80
81
82
83
84
85
86
87 @RestService(name = "assetManager", title = "AssetManager",
88 notes = {
89 "All paths are relative to the REST endpoint base (something like http://your.server/files)",
90 "If you notice that this service is not working as expected, there might be a bug! "
91 + "You should file an error report with your server logs from the time when the error occurred: "
92 + "<a href=\"http://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
93 },
94 abstractText = "This service indexes and queries available (distributed) episodes.")
95 public abstract class AbstractAssetManagerRestEndpoint extends AbstractJobProducerEndpoint {
96 protected static final Logger logger = LoggerFactory.getLogger(AbstractAssetManagerRestEndpoint.class);
97
98 private final Gson gson = new Gson();
99
100 private final java.lang.reflect.Type stringMapType = new TypeToken<Map<String, String>>() { }.getType();
101
102 public abstract AssetManager getAssetManager();
103
104
105
106
107 @POST
108 @Path("add")
109 @RestQuery(
110 name = "add",
111 description = "Adds a media package to the asset manager. This method is deprecated in "
112 + "favor of method POST 'snapshot'.",
113 restParameters = {
114 @RestParameter(
115 name = "mediapackage",
116 isRequired = true,
117 type = Type.TEXT,
118 description = "The media package to add to the search index.")},
119 responses = {
120 @RestResponse(
121 description = "The media package was added, no content to return.",
122 responseCode = SC_NO_CONTENT),
123 @RestResponse(
124 description = "Not allowed to add a media package.",
125 responseCode = SC_FORBIDDEN),
126 @RestResponse(
127 description = "There has been an internal error and the media package could not be added",
128 responseCode = SC_INTERNAL_SERVER_ERROR)},
129 returnDescription = "No content is returned.")
130 @Deprecated
131 public Response add(@FormParam("mediapackage") final MediaPackageImpl mediaPackage) {
132 return snapshot(mediaPackage);
133 }
134
135 @POST
136 @Path("snapshot")
137 @RestQuery(name = "snapshot", description = "Take a versioned snapshot of a media package.",
138 restParameters = {
139 @RestParameter(
140 name = "mediapackage",
141 isRequired = true,
142 type = Type.TEXT,
143 description = "The media package to take a snapshot from.")},
144 responses = {
145 @RestResponse(
146 description = "A snapshot of the media package has been taken, no content to return.",
147 responseCode = SC_NO_CONTENT),
148 @RestResponse(
149 description = "Not allowed to take a snapshot.",
150 responseCode = SC_FORBIDDEN),
151 @RestResponse(
152 description = "There has been an internal error and no snapshot could be taken.",
153 responseCode = SC_INTERNAL_SERVER_ERROR)},
154 returnDescription = "No content is returned.")
155 public Response snapshot(@FormParam("mediapackage") final MediaPackageImpl mediaPackage) {
156 try {
157 getAssetManager().takeSnapshot(DEFAULT_OWNER, mediaPackage);
158 return noContent();
159 } catch (Exception e) {
160 return handleException(e);
161 }
162 }
163
164 @POST
165 @Path("updateIndex")
166 @RestQuery(name = "updateIndex",
167 description = "Trigger search index update for event. The usage of this is limited to global administrators.",
168 restParameters = {
169 @RestParameter(
170 name = "id",
171 isRequired = true,
172 type = STRING,
173 description = "The event ID to trigger an index update for.")},
174 responses = {
175 @RestResponse(
176 description = "Update successfully triggered.",
177 responseCode = SC_NO_CONTENT),
178 @RestResponse(
179 description = "Not allowed to trigger update.",
180 responseCode = SC_FORBIDDEN),
181 @RestResponse(
182 description = "No such event found.",
183 responseCode = SC_NOT_FOUND)},
184 returnDescription = "No content is returned.")
185 public Response indexUpdate(@FormParam("id") final String id) {
186 try {
187 getAssetManager().triggerIndexUpdate(id);
188 return noContent();
189 } catch (UnauthorizedException e) {
190 return forbidden();
191 } catch (NotFoundException e) {
192 return notFound();
193 } catch (Exception e) {
194 return handleException(e);
195 }
196 }
197
198 @DELETE
199 @Path("delete/{id}")
200 @RestQuery(name = "deleteSnapshots",
201 description = "Removes snapshots of an episode, owned by the default owner from the asset manager.",
202 pathParameters = {
203 @RestParameter(
204 name = "id",
205 isRequired = true,
206 type = Type.STRING,
207 description = "The media package ID of the episode whose snapshots shall be removed"
208 + " from the asset manager.")},
209 responses = {
210 @RestResponse(
211 description = "Snapshots have been removed, no content to return.",
212 responseCode = SC_NO_CONTENT),
213 @RestResponse(
214 description = "The episode does either not exist or no snapshots are owned by the default owner.",
215 responseCode = SC_NOT_FOUND),
216 @RestResponse(
217 description = "Not allowed to delete this episode.",
218 responseCode = SC_FORBIDDEN),
219 @RestResponse(
220 description = "There has been an internal error and the episode could not be deleted.",
221 responseCode = SC_INTERNAL_SERVER_ERROR)},
222 returnDescription = "No content is returned.")
223 public Response delete(@PathParam("id") final String mediaPackageId) {
224 if (StringUtils.isEmpty(mediaPackageId)) {
225 return notFound();
226 }
227 try {
228 if (getAssetManager().deleteSnapshots(mediaPackageId) > 0) {
229 return noContent();
230 }
231 return notFound();
232 } catch (Exception e) {
233 return handleException(e);
234 }
235 }
236
237 @GET
238 @Produces(MediaType.TEXT_XML)
239 @Path("episode/{mediaPackageID}")
240 @RestQuery(name = "getLatestEpisode",
241 description = "Get the media package from the last snapshot of an episode.",
242 returnDescription = "The media package",
243 pathParameters = {
244 @RestParameter(
245 name = "mediaPackageID",
246 description = "the media package ID",
247 isRequired = true,
248 type = STRING)
249 },
250 responses = {
251 @RestResponse(responseCode = SC_OK, description = "Media package returned"),
252 @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found"),
253 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not allowed to read media package."),
254 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "There has been an internal error.")
255 })
256 public Response getMediaPackage(@PathParam("mediaPackageID") final String mediaPackageId) {
257
258 try {
259 Optional<MediaPackage> mp = getAssetManager().getMediaPackage(mediaPackageId);
260
261 if (mp.isPresent()) {
262 return ok(mp.get());
263 } else {
264 return notFound();
265 }
266 } catch (Exception e) {
267 return handleException(e);
268 }
269 }
270
271 @GET
272 @Path("assets/{mediaPackageID}/{mediaPackageElementID}/{version}/{filename}")
273 @RestQuery(name = "getAsset",
274 description = "Get an asset",
275 returnDescription = "The file",
276 pathParameters = {
277 @RestParameter(
278 name = "mediaPackageID",
279 description = "the media package identifier",
280 isRequired = true,
281 type = STRING),
282 @RestParameter(
283 name = "mediaPackageElementID",
284 description = "the media package element identifier",
285 isRequired = true,
286 type = STRING),
287 @RestParameter(
288 name = "version",
289 description = "the media package version",
290 isRequired = true,
291 type = STRING),
292 @RestParameter(
293 name = "filename",
294 description = "a descriptive filename used as the download filename",
295 isRequired = false,
296 type = STRING)},
297 responses = {
298 @RestResponse(
299 responseCode = SC_OK,
300 description = "File returned"),
301 @RestResponse(
302 responseCode = SC_NOT_FOUND,
303 description = "Not found"),
304 @RestResponse(
305 responseCode = SC_NOT_MODIFIED,
306 description = "If file not modified"),
307 @RestResponse(
308 description = "Not allowed to read assets of this snapshot.",
309 responseCode = SC_FORBIDDEN),
310 @RestResponse(
311 description = "There has been an internal error.",
312 responseCode = SC_INTERNAL_SERVER_ERROR)})
313 public Response getAsset(@PathParam("mediaPackageID") final String mediaPackageID,
314 @PathParam("mediaPackageElementID") final String mediaPackageElementID,
315 @PathParam("version") final String version,
316 @PathParam("filename") String fileName,
317 @HeaderParam("If-None-Match") String ifNoneMatch) {
318
319 try {
320 final var v = getAssetManager().toVersion(version);
321 if (v.isPresent()) {
322 var assetOpt = getAssetManager().getAsset(v.get(), mediaPackageID, mediaPackageElementID);
323 if (assetOpt.isPresent()) {
324 var asset = assetOpt.get();
325
326 if (StringUtils.isNotBlank(ifNoneMatch)) {
327 Checksum checksum = asset.getChecksum();
328
329 if (checksum != null && checksum.getType().equals(ChecksumType.DEFAULT_TYPE)) {
330 String md5 = checksum.getValue();
331
332 if (md5.equals(ifNoneMatch)) {
333 return Response.notModified(md5).build();
334 }
335 }
336 else {
337 logger.warn("Checksum of asset {} of media package {} is of incorrect type or missing",
338 mediaPackageElementID, mediaPackageID);
339 }
340 }
341
342 if (StringUtils.isBlank(fileName)) {
343 String suffix = "unknown";
344 if (asset.getMimeType().isPresent()) {
345 var mimetype = asset.getMimeType().get();
346 if (mimetype.getSuffix().isPresent()) {
347 suffix = mimetype.getSuffix().get();
348 }
349 }
350 fileName = mediaPackageElementID
351 .concat(".")
352 .concat(suffix);
353 }
354
355
356 Optional<Long> length = asset.getSize() > 0 ? Optional.of(asset.getSize()) : Optional.empty();
357 return ok(asset.getInputStream(),
358 asset.getMimeType().isPresent()
359 ? Optional.of(asset.getMimeType().get().toString())
360 : Optional.empty(),
361 length,
362 Optional.of(fileName));
363 }
364
365 return notFound();
366 }
367
368 return badRequest("malformed version");
369 } catch (Exception e) {
370 return handleException(e);
371 }
372 }
373
374 @GET
375 @Produces(MediaType.APPLICATION_JSON)
376 @Path("{mediaPackageID}/properties.json")
377 @RestQuery(name = "getProperties",
378 description = "Get stored properties for an episode.",
379 returnDescription = "Properties as JSON",
380 pathParameters = {
381 @RestParameter(
382 name = "mediaPackageID",
383 description = "the media package ID",
384 isRequired = true,
385 type = STRING)
386 }, restParameters = {
387 @RestParameter(
388 name = "namespace",
389 description = "property namespace",
390 isRequired = false,
391 type = STRING)
392 },
393 responses = {
394 @RestResponse(responseCode = SC_OK, description = "Media package returned"),
395 @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found"),
396 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not allowed to read media package."),
397 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "There has been an internal error.")
398 })
399 public Response getProperties(@PathParam("mediaPackageID") final String mediaPackageId,
400 @FormParam("namespace") final String namespace) {
401 try {
402 getAssetManager().selectProperties(mediaPackageId, namespace);
403
404
405 HashMap<String, HashMap<String, String>> properties = new HashMap<>();
406 for (final Property property : getAssetManager().selectProperties(mediaPackageId, namespace)) {
407 final String key = property.getId().getNamespace() + "." + property.getId().getName();
408 final HashMap<String, String> val = new HashMap<>();
409 val.put("type", property.getValue().getType().getClass().getSimpleName());
410 val.put("value", property.getValue().get().toString());
411 properties.put(key, val);
412 }
413 return ok(gson.toJson(properties));
414 } catch (Exception e) {
415 return handleException(e);
416 }
417 }
418
419 @GET
420 @Produces(MediaType.APPLICATION_JSON)
421 @Path("{mediaPackageID}/workflowProperties.json")
422 @RestQuery(name = "getWorkflowProperties",
423 description = "Get stored workflow properties for an episode.",
424 returnDescription = "Properties as JSON",
425 pathParameters = {
426 @RestParameter(
427 name = "mediaPackageID",
428 description = "the media package ID",
429 isRequired = true,
430 type = STRING)
431 },
432 responses = {
433 @RestResponse(responseCode = SC_OK, description = "Media package returned"),
434 @RestResponse(responseCode = SC_OK, description = "Invalid parameters"),
435 @RestResponse(responseCode = SC_NOT_FOUND, description = "Not found"),
436 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not allowed to read media package."),
437 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "There has been an internal error.")
438 })
439 public Response getWorkflowProperties(@PathParam("mediaPackageID") final String mediaPackageId) {
440 try {
441
442 HashMap<String, String> properties = new HashMap<>();
443 for (final Property property
444 : getAssetManager().selectProperties(mediaPackageId, WORKFLOW_PROPERTIES_NAMESPACE)) {
445 properties.put(property.getId().getName(), property.getValue().get(Value.STRING));
446 }
447 return ok(gson.toJson(properties));
448 } catch (Exception e) {
449 return handleException(e);
450 }
451 }
452
453
454 @POST
455 @Path("{mediaPackageID}/workflowProperties")
456 @RestQuery(name = "setWorkflowProperties",
457 description = "Set additional workflow properties",
458 pathParameters = {
459 @RestParameter(
460 name = "mediaPackageID",
461 description = "the media package ID",
462 isRequired = true,
463 type = STRING)
464 },
465 restParameters = {
466 @RestParameter(
467 name = "properties",
468 isRequired = true,
469 type = STRING,
470 description = "JSON object containing new properties")
471 },
472 responses = {
473 @RestResponse(description = "Properties successfully set", responseCode = SC_CREATED),
474 @RestResponse(description = "Invalid data", responseCode = SC_BAD_REQUEST),
475 @RestResponse(description = "Internal error", responseCode = SC_INTERNAL_SERVER_ERROR) },
476 returnDescription = "Returned status code indicates success")
477 public Response setWorkflowProperties(@PathParam("mediaPackageID") final String mediaPackageId,
478 @FormParam("properties") final String propertiesJSON) {
479 Map<String, String> properties;
480 try {
481 properties = gson.fromJson(propertiesJSON, stringMapType);
482 } catch (Exception e) {
483 return badRequest();
484 }
485 for (final Map.Entry<String, String> entry : properties.entrySet()) {
486 final PropertyId propertyId = PropertyId.mk(mediaPackageId, WORKFLOW_PROPERTIES_NAMESPACE, entry.getKey());
487 final Property property = Property.mk(propertyId, Value.mk(entry.getValue()));
488 if (!getAssetManager().setProperty(property)) {
489 return notFound();
490 }
491 }
492 return noContent();
493 }
494
495
496 public static Response handleException(Exception e) {
497 logger.debug("Error calling REST method", e);
498 Throwable cause = e;
499 if (e.getCause() != null) {
500 cause = e.getCause();
501 }
502 if (cause instanceof UnauthorizedException) {
503 return forbidden();
504 }
505
506 throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
507 }
508 }