/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.submit.step;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.UUID;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.app.util.SubmissionInfo;
import org.dspace.app.util.Util;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.*;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamFormatService;
import org.dspace.core.Context;
import org.dspace.curate.Curator;
import org.dspace.submit.AbstractProcessingStep;
/**
* Upload step for DSpace. Processes the actual upload of files
* for an item being submitted into DSpace.
* <P>
* This class performs all the behind-the-scenes processing that
* this particular step requires. This class's methods are utilized
* by both the JSP-UI and the Manakin XML-UI
*
* @see org.dspace.app.util.SubmissionConfig
* @see org.dspace.app.util.SubmissionStepConfig
* @see org.dspace.submit.AbstractProcessingStep
*
* @author Tim Donohue
*/
public class UploadStep extends AbstractProcessingStep
{
/** Button to upload a file * */
public static final String SUBMIT_UPLOAD_BUTTON = "submit_upload";
/** Button to skip uploading a file * */
public static final String SUBMIT_SKIP_BUTTON = "submit_skip";
/** Button to submit more files * */
public static final String SUBMIT_MORE_BUTTON = "submit_more";
/** Button to cancel editing of file info * */
public static final String CANCEL_EDIT_BUTTON = "submit_edit_cancel";
/***************************************************************************
* STATUS / ERROR FLAGS (returned by doProcessing() if an error occurs or
* additional user interaction may be required)
*
* (Do NOT use status of 0, since it corresponds to STATUS_COMPLETE flag
* defined in the JSPStepManager class)
**************************************************************************/
// integrity error occurred
public static final int STATUS_INTEGRITY_ERROR = 1;
// error in uploading file
public static final int STATUS_UPLOAD_ERROR = 2;
// error - no files uploaded!
public static final int STATUS_NO_FILES_ERROR = 5;
// format of uploaded file is unknown
public static final int STATUS_UNKNOWN_FORMAT = 10;
// virus checker unavailable ?
public static final int STATUS_VIRUS_CHECKER_UNAVAILABLE = 14;
// file failed virus check
public static final int STATUS_CONTAINS_VIRUS = 16;
// edit file information
public static final int STATUS_EDIT_BITSTREAM = 20;
// return from editing file information
public static final int STATUS_EDIT_COMPLETE = 25;
/** log4j logger */
private static final Logger log = Logger.getLogger(UploadStep.class);
/** is the upload required? */
protected boolean fileRequired = configurationService.getBooleanProperty("webui.submit.upload.required", true);
protected BitstreamFormatService bitstreamFormatService = ContentServiceFactory.getInstance().getBitstreamFormatService();
/**
* Do any processing of the information input by the user, and/or perform
* step processing (if no user interaction required)
* <P>
* It is this method's job to save any data to the underlying database, as
* necessary, and return error messages (if any) which can then be processed
* by the appropriate user interface (JSP-UI or XML-UI)
* <P>
* NOTE: If this step is a non-interactive step (i.e. requires no UI), then
* it should perform *all* of its processing in this method!
*
* @param context
* The relevant DSpace Context.
* @param request
* Servlet's HTTP request object.
* @param response
* Servlet's HTTP response object.
* @param subInfo
* submission info object
* @return Status or error flag which will be processed by
* doPostProcessing() below! (if STATUS_COMPLETE or 0 is returned,
* no errors occurred!)
* @throws ServletException
* A general exception a servlet can throw when it encounters difficulty.
* @throws IOException
* A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SQLException
* An exception that provides information on a database access error or other errors.
* @throws AuthorizeException
* Exception indicating the current user of the context does not have permission
* to perform a particular action.
*/
@Override
public int doProcessing(Context context, HttpServletRequest request,
HttpServletResponse response, SubmissionInfo subInfo)
throws ServletException, IOException, SQLException,
AuthorizeException
{
// get button user pressed
String buttonPressed = Util.getSubmitButton(request, NEXT_BUTTON);
// get reference to item
Item item = subInfo.getSubmissionItem().getItem();
// -----------------------------------
// Step #0: Upload new files (if any)
// -----------------------------------
String contentType = request.getContentType();
// if multipart form, then we are uploading a file
if ((contentType != null)
&& (contentType.indexOf("multipart/form-data") != -1))
{
// This is a multipart request, so it's a file upload
// (return any status messages or errors reported)
int status = processUploadFile(context, request, response, subInfo);
// if error occurred, return immediately
if (status != STATUS_COMPLETE)
{
return status;
}
}
// if user pressed jump-to button in process bar,
// return success (so that jump will occur)
if (buttonPressed.startsWith(PROGRESS_BAR_PREFIX) ||
buttonPressed.startsWith(PREVIOUS_BUTTON))
{
// check if a file is required to be uploaded
if (fileRequired && !itemService.hasUploadedFiles(item))
{
return STATUS_NO_FILES_ERROR;
}
else
{
return STATUS_COMPLETE;
}
}
// ---------------------------------------------
// Step #1: Check if this was just a request to
// edit file information.
// (or canceled editing information)
// ---------------------------------------------
// check if we're already editing a specific bitstream
if (request.getParameter("bitstream_id") != null)
{
if (buttonPressed.equals(CANCEL_EDIT_BUTTON))
{
// canceled an edit bitstream request
subInfo.setBitstream(null);
// this flag will just return us to the normal upload screen
return STATUS_EDIT_COMPLETE;
}
else
{
// load info for bitstream we are editing
Bitstream b = bitstreamService.find(context, Util.getUUIDParameter(request,
"bitstream_id"));
// save bitstream to submission info
subInfo.setBitstream(b);
}
}
else if (buttonPressed.startsWith("submit_edit_"))
{
// get ID of bitstream that was requested for editing
String bitstreamID = buttonPressed.substring("submit_edit_"
.length());
Bitstream b = bitstreamService
.find(context, UUID.fromString(bitstreamID));
// save bitstream to submission info
subInfo.setBitstream(b);
// return appropriate status flag to say we are now editing the
// bitstream
return STATUS_EDIT_BITSTREAM;
}
// ---------------------------------------------
// Step #2: Process any remove file request(s)
// ---------------------------------------------
// Remove-selected requests come from Manakin
if (buttonPressed.equalsIgnoreCase("submit_remove_selected"))
{
// this is a remove multiple request!
if (request.getParameter("remove") != null)
{
// get all files to be removed
String[] removeIDs = request.getParameterValues("remove");
// remove each file in the list
for (int i = 0; i < removeIDs.length; i++)
{
UUID id = UUID.fromString(removeIDs[i]);
int status = processRemoveFile(context, item, id);
// if error occurred, return immediately
if (status != STATUS_COMPLETE)
{
return status;
}
}
// remove current bitstream from Submission Info
subInfo.setBitstream(null);
}
}
else if (buttonPressed.startsWith("submit_remove_"))
{
// A single file "remove" button must have been pressed
UUID id = UUID.fromString(buttonPressed.substring(14));
int status = processRemoveFile(context, item, id);
// if error occurred, return immediately
if (status != STATUS_COMPLETE)
{
return status;
}
// remove current bitstream from Submission Info
subInfo.setBitstream(null);
}
// -------------------------------------------------
// Step #3: Check for a change in file description
// -------------------------------------------------
// We have to check for descriptions from users using the resumable upload
// and from users using the simple upload.
// Beginning with the resumable ones.
Enumeration<String> parameterNames = request.getParameterNames();
Map<String, String> descriptions = new HashMap<>();
while (parameterNames.hasMoreElements())
{
String name = parameterNames.nextElement();
if (StringUtils.startsWithIgnoreCase(name, "description["))
{
descriptions.put(
name.substring("description[".length(), name.length()-1),
request.getParameter(name));
}
}
if (!descriptions.isEmpty())
{
// we got descriptions from the resumable upload
if (item != null)
{
List<Bundle> bundles = itemService.getBundles(item, "ORIGINAL");
for (Bundle bundle : bundles)
{
List<Bitstream> bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams)
{
if (descriptions.containsKey(bitstream.getName()))
{
bitstream.setDescription(context, descriptions.get(bitstream.getName()));
bitstreamService.update(context, bitstream);
}
}
}
}
return STATUS_COMPLETE;
}
// Going on with descriptions from the simple upload
String fileDescription = request.getParameter("description");
if (fileDescription != null && fileDescription.length() > 0)
{
// save this file description
int status = processSaveFileDescription(context, request, response,
subInfo);
// if error occurred, return immediately
if (status != STATUS_COMPLETE)
{
return status;
}
}
// ------------------------------------------
// Step #4: Check for a file format change
// (if user had to manually specify format)
// ------------------------------------------
int formatTypeID = Util.getIntParameter(request, "format");
String formatDesc = request.getParameter("format_description");
// if a format id or description was found, then save this format!
if (formatTypeID >= 0
|| (formatDesc != null && formatDesc.length() > 0))
{
// save this specified format
int status = processSaveFileFormat(context, request, response,
subInfo);
// if error occurred, return immediately
if (status != STATUS_COMPLETE)
{
return status;
}
}
// ---------------------------------------------------
// Step #5: Check if primary bitstream has changed
// -------------------------------------------------
if (request.getParameter("primary_bitstream_id") != null)
{
List<Bundle> bundles = itemService.getBundles(item, "ORIGINAL");
if (bundles.size() > 0)
{
bundles.get(0).setPrimaryBitstreamID(bitstreamService.find(context, Util.getUUIDParameter(request,
"primary_bitstream_id")));
bundleService.update(context, bundles.get(0));
}
}
// ---------------------------------------------------
// Step #6: Determine if there is an error because no
// files have been uploaded.
// ---------------------------------------------------
//check if a file is required to be uploaded
if (fileRequired && !itemService.hasUploadedFiles(item)
&& !buttonPressed.equals(SUBMIT_MORE_BUTTON))
{
return STATUS_NO_FILES_ERROR;
}
context.dispatchEvents();
return STATUS_COMPLETE;
}
/**
* Retrieves the number of pages that this "step" extends over. This method
* is used to build the progress bar.
* <P>
* This method may just return 1 for most steps (since most steps consist of
* a single page). But, it should return a number greater than 1 for any
* "step" which spans across a number of HTML pages. For example, the
* configurable "Describe" step (configured using input-forms.xml) overrides
* this method to return the number of pages that are defined by its
* configuration file.
* <P>
* Steps which are non-interactive (i.e. they do not display an interface to
* the user) should return a value of 1, so that they are only processed
* once!
*
* @param request
* Servlet's HTTP request object.
* @param subInfo
* The current submission information object
* @return the number of pages in this step
* @throws ServletException
* A general exception a servlet can throw when it encounters difficulty.
*/
@Override
public int getNumberOfPages(HttpServletRequest request,
SubmissionInfo subInfo) throws ServletException
{
// Despite using many JSPs, this step only appears
// ONCE in the Progress Bar, so it's only ONE page
return 1;
}
// ****************************************************************
// ****************************************************************
// METHODS FOR UPLOADING FILES (and associated information)
// ****************************************************************
// ****************************************************************
/**
* Remove a file from an item
*
* @param context
* The relevant DSpace Context.
* @param item
* Item where file should be removed from
* @param bitstreamID
* The id of bitstream representing the file to remove
* @return Status or error flag which will be processed by
* UI-related code! (if STATUS_COMPLETE or 0 is returned,
* no errors occurred!)
* @throws IOException
* A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SQLException
* An exception that provides information on a database access error or other errors.
* @throws AuthorizeException
* Exception indicating the current user of the context does not have permission
* to perform a particular action.
*/
protected int processRemoveFile(Context context, Item item, UUID bitstreamID)
throws IOException, SQLException, AuthorizeException
{
Bitstream bitstream;
// Try to find bitstream
try
{
bitstream = bitstreamService.find(context, bitstreamID);
}
catch (NumberFormatException nfe)
{
bitstream = null;
}
if (bitstream == null)
{
// Invalid or mangled bitstream ID
// throw an error and return immediately
return STATUS_INTEGRITY_ERROR;
}
// remove bitstream from bundle..
// delete bundle if it's now empty
List<Bundle> bundles = bitstream.getBundles();
Bundle bundle = bundles.get(0);
bundleService.removeBitstream(context, bundle, bitstream);
List<Bitstream> bitstreams = bundle.getBitstreams();
// remove bundle if it's now empty
if (bitstreams.size() < 1)
{
itemService.removeBundle(context, item, bundle);
itemService.update(context, item);
}
// no errors occurred
return STATUS_COMPLETE;
}
/**
* Process the upload of a new file!
*
* @param context
* The relevant DSpace Context.
* @param request
* Servlet's HTTP request object.
* @param response
* Servlet's HTTP response object.
* @param subInfo
* submission info object
* @return Status or error flag which will be processed by
* UI-related code! (if STATUS_COMPLETE or 0 is returned,
* no errors occurred!)
* @throws ServletException
* A general exception a servlet can throw when it encounters difficulty.
* @throws IOException
* A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SQLException
* An exception that provides information on a database access error or other errors.
* @throws AuthorizeException
* Exception indicating the current user of the context does not have permission
* to perform a particular action.
*/
public int processUploadFile(Context context, HttpServletRequest request,
HttpServletResponse response, SubmissionInfo subInfo)
throws ServletException, IOException, SQLException,
AuthorizeException
{
boolean formatKnown = true;
boolean fileOK = false;
BitstreamFormat bf = null;
Bitstream b = null;
//NOTE: File should already be uploaded.
//Manakin does this automatically via Cocoon.
//For JSP-UI, the SubmissionController.uploadFiles() does the actual upload
Enumeration attNames = request.getAttributeNames();
//loop through our request attributes
while(attNames.hasMoreElements())
{
String attr = (String) attNames.nextElement();
//if this ends with "-path", this attribute
//represents a newly uploaded file
if(attr.endsWith("-path"))
{
//strip off the -path to get the actual parameter
//that the file was uploaded as
String param = attr.replace("-path", "");
// Load the file's path and input stream and description
String filePath = (String) request.getAttribute(param + "-path");
InputStream fileInputStream = (InputStream) request.getAttribute(param + "-inputstream");
//attempt to get description from attribute first, then direct from a parameter
String fileDescription = (String) request.getAttribute(param + "-description");
if(fileDescription==null ||fileDescription.length()==0)
{
fileDescription = request.getParameter("description");
}
// if information wasn't passed by User Interface, we had a problem
// with the upload
if (filePath == null || fileInputStream == null)
{
return STATUS_UPLOAD_ERROR;
}
if (subInfo == null)
{
// In any event, if we don't have the submission info, the request
// was malformed
return STATUS_INTEGRITY_ERROR;
}
// Create the bitstream
Item item = subInfo.getSubmissionItem().getItem();
// do we already have a bundle?
List<Bundle> bundles = itemService.getBundles(item, "ORIGINAL");
if (bundles.size() < 1)
{
// set bundle's name to ORIGINAL
b = itemService.createSingleBitstream(context, fileInputStream, item, "ORIGINAL");
}
else
{
// we have a bundle already, just add bitstream
b = bitstreamService.create(context, bundles.get(0), fileInputStream);
}
// Strip all but the last filename. It would be nice
// to know which OS the file came from.
String noPath = filePath;
while (noPath.indexOf('/') > -1)
{
noPath = noPath.substring(noPath.indexOf('/') + 1);
}
while (noPath.indexOf('\\') > -1)
{
noPath = noPath.substring(noPath.indexOf('\\') + 1);
}
b.setName(context, noPath);
b.setSource(context, filePath);
b.setDescription(context, fileDescription);
// Identify the format
bf = bitstreamFormatService.guessFormat(context, b);
b.setFormat(context, bf);
// Update to DB
bitstreamService.update(context, b);
itemService.update(context, item);
if ((bf != null) && (bf.isInternal()))
{
log.warn("Attempt to upload file format marked as internal system use only");
backoutBitstream(context, subInfo, b, item);
return STATUS_UPLOAD_ERROR;
}
// Check for virus
if (configurationService.getBooleanProperty("submission-curation.virus-scan"))
{
Curator curator = new Curator();
curator.addTask("vscan").curate(context, item);
int status = curator.getStatus("vscan");
if (status == Curator.CURATE_ERROR)
{
backoutBitstream(context, subInfo, b, item);
return STATUS_VIRUS_CHECKER_UNAVAILABLE;
}
else if (status == Curator.CURATE_FAIL)
{
backoutBitstream(context, subInfo, b, item);
return STATUS_CONTAINS_VIRUS;
}
}
// If we got this far then everything is more or less ok.
context.dispatchEvents();
// save this bitstream to the submission info, as the
// bitstream we're currently working with
subInfo.setBitstream(b);
//if format was not identified
if (bf == null)
{
return STATUS_UNKNOWN_FORMAT;
}
}//end if attribute ends with "-path"
}//end while
return STATUS_COMPLETE;
}
/*
* If we created a new Bitstream but now realise there is a problem then remove it.
*/
protected void backoutBitstream(Context context, SubmissionInfo subInfo, Bitstream b, Item item)
throws SQLException, AuthorizeException, IOException
{
// remove bitstream from bundle..
List<Bundle> bundles = b.getBundles();
if (bundles.isEmpty())
throw new SQLException("Bitstream is not in any Bundles.");
Bundle firstBundle = bundles.get(0);
bundleService.removeBitstream(context, firstBundle, b);
List<Bitstream> bitstreams = firstBundle.getBitstreams();
// remove bundle if it's now empty
if (bitstreams.isEmpty())
{
itemService.removeBundle(context, item, firstBundle);
itemService.update(context, item);
}
else
bundleService.update(context, firstBundle);
subInfo.setBitstream(null);
}
/**
* Process input from get file type page
*
* @param context
* current DSpace context
* @param request
* current servlet request object
* @param response
* current servlet response object
* @param subInfo
* submission info object
*
* @return Status or error flag which will be processed by
* UI-related code! (if STATUS_COMPLETE or 0 is returned,
* no errors occurred!)
* @throws ServletException
* A general exception a servlet can throw when it encounters difficulty.
* @throws IOException
* A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SQLException
* An exception that provides information on a database access error or other errors.
* @throws AuthorizeException
* Exception indicating the current user of the context does not have permission
* to perform a particular action.
*/
protected int processSaveFileFormat(Context context,
HttpServletRequest request, HttpServletResponse response,
SubmissionInfo subInfo) throws ServletException, IOException,
SQLException, AuthorizeException
{
if (subInfo.getBitstream() != null)
{
// Did the user select a format?
int typeID = Util.getIntParameter(request, "format");
BitstreamFormat format = bitstreamFormatService.find(context, typeID);
if (format != null)
{
subInfo.getBitstream().setFormat(context, format);
}
else
{
String userDesc = request.getParameter("format_description");
subInfo.getBitstream().setUserFormatDescription(context, userDesc);
}
// update database
bitstreamService.update(context, subInfo.getBitstream());
}
else
{
return STATUS_INTEGRITY_ERROR;
}
return STATUS_COMPLETE;
}
/**
* Process input from the "change file description" page
*
* @param context
* The relevant DSpace Context.
* @param request
* Servlet's HTTP request object.
* @param response
* Servlet's HTTP response object.
* @param subInfo
* submission info object
* @return Status or error flag which will be processed by
* UI-related code! (if STATUS_COMPLETE or 0 is returned,
* no errors occurred!)
* @throws ServletException
* A general exception a servlet can throw when it encounters difficulty.
* @throws IOException
* A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SQLException
* An exception that provides information on a database access error or other errors.
* @throws AuthorizeException
* Exception indicating the current user of the context does not have permission
* to perform a particular action.
*/
protected int processSaveFileDescription(Context context,
HttpServletRequest request, HttpServletResponse response,
SubmissionInfo subInfo) throws ServletException, IOException,
SQLException, AuthorizeException
{
if (subInfo.getBitstream() != null)
{
subInfo.getBitstream().setDescription(context,
request.getParameter("description"));
bitstreamService.update(context, subInfo.getBitstream());
context.dispatchEvents();
}
else
{
return STATUS_INTEGRITY_ERROR;
}
return STATUS_COMPLETE;
}
}