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