/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package fedora.server.rest;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import fedora.common.http.WebClient;
import fedora.server.Context;
import fedora.server.rest.RestUtil.RequestContent;
import fedora.server.storage.types.Datastream;
import fedora.server.storage.types.DatastreamDef;
import fedora.server.storage.types.MIMETypedStream;
import fedora.server.utilities.DateUtility;
/**
* A rest controller to handle CRUD operations for the Fedora datasream API
* (API-M) Request syntax:
*
* GET,PUT,POST,DELETE
* prototol://hostname:port/fedora/objects/PID/datastreams/DSID/versions ? [dateTime][parmArray]
* <ul>
* <li>protocol - either http or https.</li>
* <li>hostname - required hostname of the Fedora server.</li>
* <li>port - required port number on which the Fedora server is running.</li>
* <li>fedora - required path name for the Fedora access service.</li>
* <li>objects - required path name for the Fedora service.</li>
* <li>PID - required persistent idenitifer of the digital object.</li>
* <li>DSID - required datastream identifier for the datastream.</li>
* <li>dateTime - optional dateTime value indicating dissemination of a version
* of the digital object at the specified point in time.
* <li>parmArray - optional array of method parameters consisting of name/value
* pairs in the form parm1=value1&parm2=value2...</li>
*
* @author cuong.tran@yourmediashelf.com
* @version $Id$
*/
@Path("/{pid}/datastreams")
public class DatastreamResource extends BaseRestResource {
/**
* Inquires upon all object Datastreams to obtain datastreams contained by a
* digital object. This returns a set of datastream locations that represent
* all possible datastreams available in the object.
*
* GET /objects/{pid}/datastreams ? asOfDateTime format
*
* @param pid
* @param dateTime
* @param format
*/
@GET
public Response listDatastreams(
@PathParam(RestParam.PID)
String pid,
@QueryParam(RestParam.AS_OF_DATE_TIME)
String dateTime,
@QueryParam(RestParam.FORMAT)
@DefaultValue(HTML)
String format) {
try {
Date asOfDateTime = parseDate(dateTime);
Context context = getContext();
MediaType mime = RestHelper.getContentType(format);
DatastreamDef[] dsDefs = apiAService.listDatastreams(context, pid, asOfDateTime);
String output = getSerializer(context).dataStreamsToXML(pid, asOfDateTime, dsDefs);
if (TEXT_HTML.isCompatible(mime)) {
CharArrayWriter writer = new CharArrayWriter();
transform(output, "access/listDatastreams.xslt", writer);
output = writer.toString();
}
return Response.ok(output, mime).build();
} catch (Exception ex) {
return handleException(ex);
}
}
/**
* Invoke API-M.getDatastream(context, pid, dsID, asOfDateTime)
*
* GET /objects/{pid}/datastreams/{dsID} ? asOfDateTime & validateChecksum=true|false
*/
@Path("/{dsID}")
@GET
public Response getDatastreamProfile(
@PathParam(RestParam.PID)
String pid,
@PathParam(RestParam.DSID)
String dsID,
@QueryParam(RestParam.AS_OF_DATE_TIME)
String dateTime,
@QueryParam(RestParam.FORMAT)
@DefaultValue(HTML)
String format,
@QueryParam(RestParam.VALIDATE_CHECKSUM)
@DefaultValue("false")
boolean validateChecksum) {
try {
Date asOfDateTime = parseDate(dateTime);
Context context = getContext();
Datastream dsProfile = apiMService.getDatastream(context, pid, dsID, asOfDateTime);
if(dsProfile == null) {
return Response.status(Status.NOT_FOUND).type("text/plain").entity(
"No datastream could be found. Either there is no datastream for " +
"the digital object \""+pid+"\" with datastream ID of \""+dsID+
"\" OR there are no datastreams that match the specified " +
"date/time value of \""+dateTime+"\".").build();
}
String xml = getSerializer(context).
datastreamProfileToXML(pid, dsID, dsProfile, asOfDateTime, validateChecksum);
MediaType mime = RestHelper.getContentType(format);
if (TEXT_HTML.isCompatible(mime)) {
CharArrayWriter writer = new CharArrayWriter();
transform(xml, "management/viewDatastreamProfile.xslt", writer);
xml = writer.toString();
}
return Response.ok(xml, mime).build();
} catch (Exception ex) {
return handleException(ex);
}
}
/**
* Invoke API-A.getDatastreamDissemination(context, pid, dsID, asOfDateTime)
*
* GET /objects/{pid}/datastreams/{dsID}/content ? asOfDateTime
*/
@Path("/{dsID}/content")
@GET
public Response getDatastream(
@PathParam(RestParam.PID)
String pid,
@PathParam(RestParam.DSID)
String dsID,
@QueryParam(RestParam.AS_OF_DATE_TIME)
String dateTime,
@QueryParam(RestParam.DOWNLOAD)
String download) {
Context context = getContext();
try {
MIMETypedStream stream = apiAService.getDatastreamDissemination(
context,
pid,
dsID,
parseDate(dateTime));
if (datastreamFilenameHelper != null) {
datastreamFilenameHelper.addContentDispositionHeader(context, pid, dsID, download, parseDate(dateTime), stream);
}
return buildResponse(stream);
} catch (Exception ex) {
return handleException(ex);
}
}
/**
* Invoke API-M.purgeDatastream
*
* DELETE /objects/{pid}/datastreams/{dsID} ? startDT endDT logMessage force
*/
@Path("/{dsID}")
@DELETE
public Response deleteDatastream(
@PathParam(RestParam.PID)
String pid,
@PathParam(RestParam.DSID)
String dsID,
@QueryParam(RestParam.START_DT)
String startDT,
@QueryParam(RestParam.END_DT)
String endDT,
@QueryParam(RestParam.LOG_MESSAGE)
String logMessage,
@QueryParam(RestParam.FORCE)
@DefaultValue("false")
boolean force) {
try {
Context context = getContext();
Date startDate = DateUtility.convertStringToDate(startDT);
Date endDate = DateUtility.convertStringToDate(endDT);
apiMService.purgeDatastream(context, pid, dsID, startDate,
endDate, logMessage, force);
return Response.noContent().build();
} catch (Exception ex) {
return handleException(ex);
}
}
/**
* Modify an existing datastream.
*
* PUT /objects/{pid}/datastreams/{dsID} ? dsLocation altIDs dsLabel versionable
* dsState formatURI checksumType checksum
* logMessage force
*
* Successful Response:
* Status: 200 OK
* Content-Type: text/xml
* Body: XML datastream profile
*/
@Path("/{dsID}")
@PUT
public Response modifyDatastream(
@PathParam(RestParam.PID)
String pid,
@PathParam(RestParam.DSID)
String dsID,
@QueryParam(RestParam.DS_LOCATION)
String dsLocation,
@QueryParam(RestParam.ALT_IDS)
List<String> altIDs,
@QueryParam(RestParam.DS_LABEL)
String dsLabel,
@QueryParam(RestParam.VERSIONABLE)
@DefaultValue("true")
boolean versionable,
@QueryParam(RestParam.DS_STATE)
String dsState,
@QueryParam(RestParam.FORMAT_URI)
String formatURI,
@QueryParam(RestParam.CHECKSUM_TYPE)
String checksumType,
@QueryParam(RestParam.CHECKSUM)
String checksum,
@QueryParam(RestParam.MIME_TYPE)
String mimeType,
@QueryParam(RestParam.LOG_MESSAGE)
String logMessage,
@QueryParam(RestParam.FORCE)
@DefaultValue("false")
boolean force,
@QueryParam(RestParam.IGNORE_CONTENT)
@DefaultValue("false")
boolean ignoreContent) {
return addOrUpdateDatastream(false, pid, dsID, headers.getMediaType(), mimeType,
null, dsLocation, altIDs, dsLabel, versionable,
dsState, formatURI, checksumType, checksum,
logMessage, force, ignoreContent);
}
/**
* Add or modify a datastream.
*
* POST /objects/{pid}/datastreams/{dsID} ? controlGroup dsLocation altIDs dsLabel
* versionable dsState formatURI
* checksumType checksum logMessage
*
* Successful Response:
* Status: 201 Created
* Location: Datastream profile URL
* Content-Type: text/xml
* Body: XML datastream profile
*/
@Path("/{dsID}")
@POST
public Response addDatastream(
@PathParam(RestParam.PID)
String pid,
@PathParam(RestParam.DSID)
String dsID,
@QueryParam(RestParam.CONTROL_GROUP)
@DefaultValue("X")
String controlGroup,
@QueryParam(RestParam.DS_LOCATION)
String dsLocation,
@QueryParam(RestParam.ALT_IDS)
List<String> altIDs,
@QueryParam(RestParam.DS_LABEL)
String dsLabel,
@QueryParam(RestParam.VERSIONABLE)
@DefaultValue("true")
boolean versionable,
@QueryParam(RestParam.DS_STATE)
@DefaultValue("A")
String dsState,
@QueryParam(RestParam.FORMAT_URI)
String formatURI,
@QueryParam(RestParam.CHECKSUM_TYPE)
String checksumType,
@QueryParam(RestParam.CHECKSUM)
String checksum,
@QueryParam(RestParam.MIME_TYPE)
String mimeType,
@QueryParam(RestParam.LOG_MESSAGE)
String logMessage) {
return addOrUpdateDatastream(true, pid, dsID, headers.getMediaType(), mimeType,
controlGroup, dsLocation, altIDs, dsLabel,
versionable, dsState, formatURI, checksumType,
checksum, logMessage, false, false);
}
protected Response addOrUpdateDatastream(
boolean posted,
String pid,
String dsID,
MediaType mediaType,
String mimeType,
String controlGroup,
String dsLocation,
List<String> altIDList,
String dsLabel,
boolean versionable,
String dsState,
String formatURI,
String checksumType,
String checksum,
String logMessage,
boolean force,
boolean ignoreContent) {
try {
String[] altIDs = {};
if (altIDList != null && altIDList.size() > 0) {
altIDs = altIDList.toArray(new String[altIDList.size()]);
}
Context context = getContext();
Datastream existingDS = apiMService.getDatastream(context, pid, dsID, null);
// If a datastream is set to Deleted state, it must be set to
// another state before any other changes can be made
if(existingDS != null && existingDS.DSState.equals("D") && dsState != null) {
if(dsState.equals("A") || dsState.equals("I")) {
apiMService.setDatastreamState(context, pid, dsID,
dsState, logMessage);
existingDS.DSState = dsState;
}
}
InputStream is = null;
// Determine if datastream content is included in the request
if(!ignoreContent) {
RestUtil restUtil = new RestUtil();
RequestContent content =
restUtil.getRequestContent(servletRequest, headers);
if(content != null && content.getContentStream() != null) {
is = content.getContentStream();
// Give preference to the passed in mimeType
if(mimeType == null && content.getMimeType() != null) {
mimeType = content.getMimeType();
}
}
}
// Make sure that there is a mime type value
if(mimeType == null && mediaType != null) {
mimeType = mediaType.toString();
} else if(mimeType == null && mediaType == null) {
mimeType = existingDS.DSMIME;
}
// set default control group based on mimeType
if (dsLocation == null &&
TEXT_XML.isCompatible(MediaType.valueOf(mimeType)) &&
controlGroup == null) {
controlGroup = "X";
}
if (existingDS == null) {
if (posted) {
if ((dsLocation == null || dsLocation.equals(""))
&& ("X".equals(controlGroup) || "M".equals(controlGroup))) {
dsLocation = apiMService.putTempStream(context, is);
}
dsID = apiMService.addDatastream(context, pid, dsID, altIDs, dsLabel,
versionable, mimeType, formatURI,
dsLocation, controlGroup, dsState,
checksumType, checksum, logMessage);
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
} else {
if ("X".equals(existingDS.DSControlGrp)) {
// Inline XML can only be modified by value. If there is no stream,
// but there is a dsLocation attempt to retrieve the content.
if(is == null && dsLocation != null && !dsLocation.equals("")) {
try {
WebClient webClient = new WebClient();
is = webClient.get(dsLocation, true);
} catch(IOException ioe) {
throw new Exception("Could not retrive content from " +
dsLocation + " due to error: " +
ioe.getMessage());
}
}
apiMService.modifyDatastreamByValue(context, pid, dsID, altIDs,
dsLabel, mimeType, formatURI,
is, checksumType, checksum,
logMessage, force);
} else {
// Managed content can only be modified by reference.
// If there is no dsLocation, but there is a content stream,
// store the stream in a temporary location.
if (dsLocation == null && ("M".equals(existingDS.DSControlGrp))) {
if(is != null) {
dsLocation = apiMService.putTempStream(context, is);
} else {
dsLocation = null;
}
}
apiMService.modifyDatastreamByReference(context, pid, dsID, altIDs,
dsLabel, mimeType, formatURI,
dsLocation, checksumType,
checksum, logMessage, force);
}
if(dsState != null) {
if(dsState.equals("A") ||
dsState.equals("D") ||
dsState.equals("I")) {
if(!dsState.equals(existingDS.DSState)) {
apiMService.setDatastreamState(context, pid, dsID,
dsState, logMessage);
}
}
}
if(versionable != existingDS.DSVersionable) {
apiMService.setDatastreamVersionable(context, pid, dsID,
versionable, logMessage);
}
}
ResponseBuilder builder;
if (posted) {
builder = Response.created(uriInfo.getRequestUri().resolve(
URLEncoder.encode(dsID, DEFAULT_ENC)));
} else { // put
builder = Response.ok();
}
builder.header("Content-Type", MediaType.TEXT_XML);
Datastream dsProfile = apiMService
.getDatastream(context, pid, dsID, null);
String xml = getSerializer(context)
.datastreamProfileToXML(pid, dsID, dsProfile, null, false);
builder.entity(xml);
return builder.build();
} catch (Exception ex) {
return handleException(ex);
}
}
}