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.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   * A base REST endpoint for the {@link AssetManager}.
82   * <p>
83   * The endpoint provides assets over http (see {@link org.opencastproject.assetmanager.impl.HttpAssetProvider}).
84   * <p>
85   * No @Path annotation here since this class cannot be created by JAX-RS. Put it on the concrete implementations.
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    * @deprecated use {@link #snapshot} instead
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           // Write the file contents back
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         // none
365         return notFound();
366       }
367       // cannot parse version
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       // build map from properties
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       // build map from properties
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   /** Unify exception handling. */
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 }