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.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   * A base REST endpoint for the {@link AssetManager}.
87   * <p>
88   * The endpoint provides assets over http (see {@link org.opencastproject.assetmanager.impl.HttpAssetProvider}).
89   * <p>
90   * No @Path annotation here since this class cannot be created by JAX-RS. Put it on the concrete implementations.
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    * @deprecated use {@link #snapshot} instead
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           // Write the file contents back
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         // none
369         return notFound();
370       }
371       // cannot parse version
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       // we expect exactly one result when specifying a media package id
417       if (result.getSize() < 1) {
418         return notFound();
419       } else if (result.getSize() > 1) {
420         return serverError();
421       }
422 
423       // build map from properties
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       // we expect exactly one result when specifying a media package id
467       if (result.getSize() < 1) {
468         return notFound();
469       } else if (result.getSize() > 1) {
470         return serverError();
471       }
472 
473       // build map from properties
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   /** Unify exception handling. */
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 }