AbstractAssetManagerRestEndpoint.java
/*
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.assetmanager.impl.endpoint;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.opencastproject.assetmanager.api.AssetManager.DEFAULT_OWNER;
import static org.opencastproject.systems.OpencastConstants.WORKFLOW_PROPERTIES_NAMESPACE;
import static org.opencastproject.util.RestUtil.R.badRequest;
import static org.opencastproject.util.RestUtil.R.forbidden;
import static org.opencastproject.util.RestUtil.R.noContent;
import static org.opencastproject.util.RestUtil.R.notFound;
import static org.opencastproject.util.RestUtil.R.ok;
import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
import org.opencastproject.assetmanager.api.AssetManager;
import org.opencastproject.assetmanager.api.Property;
import org.opencastproject.assetmanager.api.PropertyId;
import org.opencastproject.assetmanager.api.Value;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageImpl;
import org.opencastproject.rest.AbstractJobProducerEndpoint;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.util.Checksum;
import org.opencastproject.util.ChecksumType;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestParameter.Type;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
* A base REST endpoint for the {@link AssetManager}.
* <p>
* The endpoint provides assets over http (see {@link org.opencastproject.assetmanager.impl.HttpAssetProvider}).
* <p>
* No @Path annotation here since this class cannot be created by JAX-RS. Put it on the concrete implementations.
*/
@RestService(name = "assetManager", title = "AssetManager",
notes = {
"All paths are relative to the REST endpoint base (something like http://your.server/files)",
"If you notice that this service is not working as expected, there might be a bug! "
+ "You should file an error report with your server logs from the time when the error occurred: "
+ "<a href=\"http://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
},
abstractText = "This service indexes and queries available (distributed) episodes.")
public abstract class AbstractAssetManagerRestEndpoint extends AbstractJobProducerEndpoint {
protected static final Logger logger = LoggerFactory.getLogger(AbstractAssetManagerRestEndpoint.class);
private final Gson gson = new Gson();
private final java.lang.reflect.Type stringMapType = new TypeToken<Map<String, String>>() { }.getType();
public abstract AssetManager getAssetManager();
/**
* @deprecated use {@link #snapshot} instead
*/
@POST
@Path("add")
@RestQuery(
name = "add",
description = "Adds a media package to the asset manager. This method is deprecated in "
+ "favor of method POST 'snapshot'.",
restParameters = {
@RestParameter(
name = "mediapackage",
isRequired = true,
type = Type.TEXT,
description = "The media package to add to the search index.")},
responses = {
@RestResponse(
description = "The media package was added, no content to return.",
responseCode = SC_NO_CONTENT),
@RestResponse(
description = "Not allowed to add a media package.",
responseCode = SC_FORBIDDEN),
@RestResponse(
description = "There has been an internal error and the media package could not be added",
responseCode = SC_INTERNAL_SERVER_ERROR)},
returnDescription = "No content is returned.")
@Deprecated
public Response add(@FormParam("mediapackage") final MediaPackageImpl mediaPackage) {
return snapshot(mediaPackage);
}
@POST
@Path("snapshot")
@RestQuery(name = "snapshot", description = "Take a versioned snapshot of a media package.",
restParameters = {
@RestParameter(
name = "mediapackage",
isRequired = true,
type = Type.TEXT,
description = "The media package to take a snapshot from.")},
responses = {
@RestResponse(
description = "A snapshot of the media package has been taken, no content to return.",
responseCode = SC_NO_CONTENT),
@RestResponse(
description = "Not allowed to take a snapshot.",
responseCode = SC_FORBIDDEN),
@RestResponse(
description = "There has been an internal error and no snapshot could be taken.",
responseCode = SC_INTERNAL_SERVER_ERROR)},
returnDescription = "No content is returned.")
public Response snapshot(@FormParam("mediapackage") final MediaPackageImpl mediaPackage) {
try {
getAssetManager().takeSnapshot(DEFAULT_OWNER, mediaPackage);
return noContent();
} catch (Exception e) {
return handleException(e);
}
}
@POST
@Path("updateIndex")
@RestQuery(name = "updateIndex",
description = "Trigger search index update for event. The usage of this is limited to global administrators.",
restParameters = {
@RestParameter(
name = "id",
isRequired = true,
type = STRING,
description = "The event ID to trigger an index update for.")},
responses = {
@RestResponse(
description = "Update successfully triggered.",
responseCode = SC_NO_CONTENT),
@RestResponse(
description = "Not allowed to trigger update.",
responseCode = SC_FORBIDDEN),
@RestResponse(
description = "No such event found.",
responseCode = SC_NOT_FOUND)},
returnDescription = "No content is returned.")
public Response indexUpdate(@FormParam("id") final String id) {
try {
getAssetManager().triggerIndexUpdate(id);
return noContent();
} catch (UnauthorizedException e) {
return forbidden();
} catch (NotFoundException e) {
return notFound();
} catch (Exception e) {
return handleException(e);
}
}
@DELETE
@Path("delete/{id}")
@RestQuery(name = "deleteSnapshots",
description = "Removes snapshots of an episode, owned by the default owner from the asset manager.",
pathParameters = {
@RestParameter(
name = "id",
isRequired = true,
type = Type.STRING,
description = "The media package ID of the episode whose snapshots shall be removed"
+ " from the asset manager.")},
responses = {
@RestResponse(
description = "Snapshots have been removed, no content to return.",
responseCode = SC_NO_CONTENT),
@RestResponse(
description = "The episode does either not exist or no snapshots are owned by the default owner.",
responseCode = SC_NOT_FOUND),
@RestResponse(
description = "Not allowed to delete this episode.",
responseCode = SC_FORBIDDEN),
@RestResponse(
description = "There has been an internal error and the episode could not be deleted.",
responseCode = SC_INTERNAL_SERVER_ERROR)},
returnDescription = "No content is returned.")
public Response delete(@PathParam("id") final String mediaPackageId) {
if (StringUtils.isEmpty(mediaPackageId)) {
return notFound();
}
try {
if (getAssetManager().deleteSnapshots(mediaPackageId) > 0) {
return noContent();
}
return notFound();
} catch (Exception e) {
return handleException(e);
}
}
@GET
@Produces(MediaType.TEXT_XML)
@Path("episode/{mediaPackageID}")
@RestQuery(name = "getLatestEpisode",
description = "Get the media package from the last snapshot of an episode.",
returnDescription = "The media package",
pathParameters = {
@RestParameter(
name = "mediaPackageID",
description = "the media package ID",
isRequired = true,
type = STRING)
},
responses = {
@RestResponse(responseCode = SC_OK, description = "Media package returned"),
@RestResponse(responseCode = SC_NOT_FOUND, description = "Not found"),
@RestResponse(responseCode = SC_FORBIDDEN, description = "Not allowed to read media package."),
@RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "There has been an internal error.")
})
public Response getMediaPackage(@PathParam("mediaPackageID") final String mediaPackageId) {
try {
Optional<MediaPackage> mp = getAssetManager().getMediaPackage(mediaPackageId);
if (mp.isPresent()) {
return ok(mp.get());
} else {
return notFound();
}
} catch (Exception e) {
return handleException(e);
}
}
@GET
@Path("assets/{mediaPackageID}/{mediaPackageElementID}/{version}/{filename}")
@RestQuery(name = "getAsset",
description = "Get an asset",
returnDescription = "The file",
pathParameters = {
@RestParameter(
name = "mediaPackageID",
description = "the media package identifier",
isRequired = true,
type = STRING),
@RestParameter(
name = "mediaPackageElementID",
description = "the media package element identifier",
isRequired = true,
type = STRING),
@RestParameter(
name = "version",
description = "the media package version",
isRequired = true,
type = STRING),
@RestParameter(
name = "filename",
description = "a descriptive filename used as the download filename",
isRequired = false,
type = STRING)},
responses = {
@RestResponse(
responseCode = SC_OK,
description = "File returned"),
@RestResponse(
responseCode = SC_NOT_FOUND,
description = "Not found"),
@RestResponse(
responseCode = SC_NOT_MODIFIED,
description = "If file not modified"),
@RestResponse(
description = "Not allowed to read assets of this snapshot.",
responseCode = SC_FORBIDDEN),
@RestResponse(
description = "There has been an internal error.",
responseCode = SC_INTERNAL_SERVER_ERROR)})
public Response getAsset(@PathParam("mediaPackageID") final String mediaPackageID,
@PathParam("mediaPackageElementID") final String mediaPackageElementID,
@PathParam("version") final String version,
@PathParam("filename") String fileName,
@HeaderParam("If-None-Match") String ifNoneMatch) {
try {
final var v = getAssetManager().toVersion(version);
if (v.isPresent()) {
var assetOpt = getAssetManager().getAsset(v.get(), mediaPackageID, mediaPackageElementID);
if (assetOpt.isPresent()) {
var asset = assetOpt.get();
if (StringUtils.isNotBlank(ifNoneMatch)) {
Checksum checksum = asset.getChecksum();
if (checksum != null && checksum.getType().equals(ChecksumType.DEFAULT_TYPE)) {
String md5 = checksum.getValue();
if (md5.equals(ifNoneMatch)) {
return Response.notModified(md5).build();
}
}
else {
logger.warn("Checksum of asset {} of media package {} is of incorrect type or missing",
mediaPackageElementID, mediaPackageID);
}
}
if (StringUtils.isBlank(fileName)) {
String suffix = "unknown";
if (asset.getMimeType().isPresent()) {
var mimetype = asset.getMimeType().get();
if (mimetype.getSuffix().isPresent()) {
suffix = mimetype.getSuffix().get();
}
}
fileName = mediaPackageElementID
.concat(".")
.concat(suffix);
}
// Write the file contents back
Optional<Long> length = asset.getSize() > 0 ? Optional.of(asset.getSize()) : Optional.empty();
return ok(asset.getInputStream(),
asset.getMimeType().isPresent()
? Optional.of(asset.getMimeType().get().toString())
: Optional.empty(),
length,
Optional.of(fileName));
}
// none
return notFound();
}
// cannot parse version
return badRequest("malformed version");
} catch (Exception e) {
return handleException(e);
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{mediaPackageID}/properties.json")
@RestQuery(name = "getProperties",
description = "Get stored properties for an episode.",
returnDescription = "Properties as JSON",
pathParameters = {
@RestParameter(
name = "mediaPackageID",
description = "the media package ID",
isRequired = true,
type = STRING)
}, restParameters = {
@RestParameter(
name = "namespace",
description = "property namespace",
isRequired = false,
type = STRING)
},
responses = {
@RestResponse(responseCode = SC_OK, description = "Media package returned"),
@RestResponse(responseCode = SC_NOT_FOUND, description = "Not found"),
@RestResponse(responseCode = SC_FORBIDDEN, description = "Not allowed to read media package."),
@RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "There has been an internal error.")
})
public Response getProperties(@PathParam("mediaPackageID") final String mediaPackageId,
@FormParam("namespace") final String namespace) {
try {
getAssetManager().selectProperties(mediaPackageId, namespace);
// build map from properties
HashMap<String, HashMap<String, String>> properties = new HashMap<>();
for (final Property property : getAssetManager().selectProperties(mediaPackageId, namespace)) {
final String key = property.getId().getNamespace() + "." + property.getId().getName();
final HashMap<String, String> val = new HashMap<>();
val.put("type", property.getValue().getType().getClass().getSimpleName());
val.put("value", property.getValue().get().toString());
properties.put(key, val);
}
return ok(gson.toJson(properties));
} catch (Exception e) {
return handleException(e);
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{mediaPackageID}/workflowProperties.json")
@RestQuery(name = "getWorkflowProperties",
description = "Get stored workflow properties for an episode.",
returnDescription = "Properties as JSON",
pathParameters = {
@RestParameter(
name = "mediaPackageID",
description = "the media package ID",
isRequired = true,
type = STRING)
},
responses = {
@RestResponse(responseCode = SC_OK, description = "Media package returned"),
@RestResponse(responseCode = SC_OK, description = "Invalid parameters"),
@RestResponse(responseCode = SC_NOT_FOUND, description = "Not found"),
@RestResponse(responseCode = SC_FORBIDDEN, description = "Not allowed to read media package."),
@RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "There has been an internal error.")
})
public Response getWorkflowProperties(@PathParam("mediaPackageID") final String mediaPackageId) {
try {
// build map from properties
HashMap<String, String> properties = new HashMap<>();
for (final Property property
: getAssetManager().selectProperties(mediaPackageId, WORKFLOW_PROPERTIES_NAMESPACE)) {
properties.put(property.getId().getName(), property.getValue().get(Value.STRING));
}
return ok(gson.toJson(properties));
} catch (Exception e) {
return handleException(e);
}
}
@POST
@Path("{mediaPackageID}/workflowProperties")
@RestQuery(name = "setWorkflowProperties",
description = "Set additional workflow properties",
pathParameters = {
@RestParameter(
name = "mediaPackageID",
description = "the media package ID",
isRequired = true,
type = STRING)
},
restParameters = {
@RestParameter(
name = "properties",
isRequired = true,
type = STRING,
description = "JSON object containing new properties")
},
responses = {
@RestResponse(description = "Properties successfully set", responseCode = SC_CREATED),
@RestResponse(description = "Invalid data", responseCode = SC_BAD_REQUEST),
@RestResponse(description = "Internal error", responseCode = SC_INTERNAL_SERVER_ERROR) },
returnDescription = "Returned status code indicates success")
public Response setWorkflowProperties(@PathParam("mediaPackageID") final String mediaPackageId,
@FormParam("properties") final String propertiesJSON) {
Map<String, String> properties;
try {
properties = gson.fromJson(propertiesJSON, stringMapType);
} catch (Exception e) {
return badRequest();
}
for (final Map.Entry<String, String> entry : properties.entrySet()) {
final PropertyId propertyId = PropertyId.mk(mediaPackageId, WORKFLOW_PROPERTIES_NAMESPACE, entry.getKey());
final Property property = Property.mk(propertyId, Value.mk(entry.getValue()));
if (!getAssetManager().setProperty(property)) {
return notFound();
}
}
return noContent();
}
/** Unify exception handling. */
public static Response handleException(Exception e) {
logger.debug("Error calling REST method", e);
Throwable cause = e;
if (e.getCause() != null) {
cause = e.getCause();
}
if (cause instanceof UnauthorizedException) {
return forbidden();
}
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
}
}