package org.dspace.submit.step.jorum; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.Enumeration; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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.Bitstream; import org.dspace.content.BitstreamFormat; import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.FormatIdentifier; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; import org.dspace.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.handle.HandleManager; import org.dspace.submit.AbstractProcessingStep; import uk.ac.jorum.dspace.utils.BundleUtils; import uk.ac.jorum.utils.URLChecker; import uk.ac.jorum.utils.VirusChecker; /** * 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 * @version $Revision: 3705 $ */ /** * @author cgormle1 * */ public class JorumUploadStep 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 AbstractProcessingStep 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; // edit file information public static final int STATUS_EDIT_BITSTREAM = 20; // return from editing file information public static final int STATUS_EDIT_COMPLETE = 25; /***Added by CG ****/ /** Indicates a URL was not supplied **/ public static final int STATUS_MISSING_URL = 6; public static final int ALL_BUNDLES_REMOVED = 7; public static final int SINGLE_BUNDLE_REMOVED = 8; public static final int VIRUS_CHECK_FAILED = 9; private static final String URL_IDENTIFIER = "identifier"; // the metadata language qualifier private static final String LANGUAGE_QUALIFIER = BundleUtils.getDefaultLanguageQualifier(); /** log4j logger */ private static Logger log = Logger.getLogger(JorumUploadStep.class); /*******/ /** is the upload required? */ private boolean fileRequired = ConfigurationManager.getBooleanProperty("webui.submit.upload.required", true); /** * 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 * 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 * doPostProcessing() below! (if STATUS_COMPLETE or 0 is returned, * no errors occurred!) */ 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(); /***Added by CG ****/ //Deal with URL submissions if (BundleUtils.checkUrl(subInfo)) { //Get url from request String url = request.getParameter("url"); // GWaller 20/8/09 Url can be http, https, ftp etc - use utility class for this String trimmedURL = url.trim(); int urlSchemeLen = URLChecker.isURL(trimmedURL); if (trimmedURL.length() == 0 || (trimmedURL.length() == urlSchemeLen) ) { // If url not entered in form, return error to prevent user from proceeding. // Error picked up in JorumUploadStep xmlui class return STATUS_MISSING_URL; } Bundle bundle = getURLBundle(item); // set the format BitstreamFormat bs_format = BitstreamFormat.findByShortDescription(context, "Text"); // set the URL as the primary bitstream // GWaller 20/8/09 Use trimmed URL not the user submitted one BundleUtils.setBitstreamFromBytes(bundle, trimmedURL, bs_format, trimmedURL.getBytes(), true); //Set the url in dc.identifier field associated with the object // GWaller 20/8/09 Use trimmed URL not the user submitted one BundleUtils.clearAndSetMetadataElement(trimmedURL, item, Constants.DC_SCHEMA, URL_IDENTIFIER, null, LANGUAGE_QUALIFIER); //All saved and ok - allow user to proceed to the next step return STATUS_COMPLETE; } /*******/ // ----------------------------------- // 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)) { // check if a file is required to be uploaded if (fileRequired && !item.hasUploadedFiles()) { 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 = Bitstream.find(context, Integer.parseInt(request.getParameter("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 = Bitstream.find(context, Integer.parseInt(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"); int status = 0; // remove each file in the list for (int i = 0; i < removeIDs.length; i++) { int id = Integer.parseInt(removeIDs[i]); status = processRemoveFile(context, item, id, subInfo); // Added by CG // We should return status complete if all bundles removed and metatdata // deleted. If not, red error message would have been displayed if(status == ALL_BUNDLES_REMOVED){ subInfo.setBitstream(null); return STATUS_COMPLETE; } } // remove current bitstream from Submission Info subInfo.setBitstream(null); // if one or more (but not all removed) we can just return status_complete, otherwise red error message // would be displayed if(status == SINGLE_BUNDLE_REMOVED){ return STATUS_COMPLETE; } } } else if (buttonPressed.startsWith("submit_remove_")) { // A single file "remove" button must have been pressed int id = Integer.parseInt(buttonPressed.substring(14)); int status = processRemoveFile(context, item, id, subInfo); // 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 // ------------------------------------------------- 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) { Bundle[] bundles = item.getBundles("ORIGINAL"); bundles[0].setPrimaryBitstreamID(new Integer(request.getParameter("primary_bitstream_id")).intValue()); bundles[0].update(); } // --------------------------------------------------- // 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 && !item.hasUploadedFiles()) { return STATUS_NO_FILES_ERROR; } // commit all changes to database context.commit(); 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 * The HTTP Request * @param subInfo * The current submission information object * * @return the number of pages in this step */ 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 * current 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!) */ protected int processRemoveFile(Context context, Item item, int bitstreamID, SubmissionInfo subInfo) throws IOException, SQLException, AuthorizeException { Bitstream bitstream; // Try to find bitstream try { bitstream = Bitstream.find(context, bitstreamID); } catch (NumberFormatException nfe) { return STATUS_INTEGRITY_ERROR; } Context itemContext = item.getContext(); int status = 0; try { context.turnOffAuthorisationSystem(); if (itemContext != null) { itemContext.turnOffAuthorisationSystem(); } // Check the related items status = checkRelated(context, item, bitstreamID, subInfo); // Now check the wrapper item status = checkWrapper(context, item, bitstreamID, subInfo, status); // A final check to ensure that the workspace position is reset int bundlesLength = item.getBundles().length; if (item.getBundles("ORIGINAL").length > 0) { // if there are no bitstreams in the Original bundle and no other bundles, // reset stage reached and return ALL_BUNDLES_REMOVED if ((item.getBundles("ORIGINAL")[0].getBitstreams().length == 0 && bundlesLength == 1)) { resetStageReached(subInfo); status = ALL_BUNDLES_REMOVED; } } else if (bundlesLength == 0) { // If there are no bundles at all, reset the stage reached resetStageReached(subInfo); status = ALL_BUNDLES_REMOVED; } } finally { // MUST RESET auth state in context !!! context.restoreAuthSystemState(); if (itemContext != null) { itemContext.restoreAuthSystemState(); } } return status; } /** * Cycle through the wrapper bitstreams to check for a match and remove * as appropriate * @param context * @param item * @param bitstreamID * @param subInfo * @param status * @return * @throws SQLException * @throws AuthorizeException * @throws IOException */ private int checkWrapper(Context context, Item item, int bitstreamID, SubmissionInfo subInfo, int status) throws SQLException, AuthorizeException, IOException { Bundle[] wrapperBundles = item.getBundles(); for (Bundle bundle : wrapperBundles) { Bitstream[] wrapperBitstreams = bundle.getBitstreams(); for (Bitstream bStream : wrapperBitstreams) { int wrapperBSID = bStream.getID(); // check if there's a bitstream in the wrapper object that should be removed if (bitstreamID == wrapperBSID) { status = removeWrapperBitstream(context, item, bundle, bStream, subInfo); if (status != ALL_BUNDLES_REMOVED) { status = SINGLE_BUNDLE_REMOVED; } } } } return status; } /** * We start with the wrapper item and the id of the bitstream to be deleted from the related item * Cycle through relatedCPs of wrapper item * For each, get the name (which is the handle of the related item) * Locate related item using this handle and get the bitstreams in the archivedCP of related item * Check if bitstream id matches the one to be deleted * If so, delete the related item and then the relatedCP bitstream in the wrapper item * * @param context * @param item * @param bitstreamID * @param subInfo * @param status * @return * @throws SQLException * @throws AuthorizeException * @throws IOException */ private int checkRelated(Context context, Item item, int bitstreamID, SubmissionInfo subInfo) throws SQLException, AuthorizeException, IOException { int status = 0; Bundle[] related = item.getBundles(Constants.RELATED_CONTENT_PACKAGE_BUNDLE); if (related.length > 0) { //Get the actual archived cp bundles from the related CPs for (Bundle bundle : related) { Bitstream[] bitstreams = bundle.getBitstreams(); // Cycle through the related items // for each, get the ArchivedCP bundle for (Bitstream relatedBitstream : bitstreams) { // Get the name of the related item Item relatedItem = (Item) HandleManager.resolveToObject(context, relatedBitstream.getName()); Bundle archived = relatedItem.getBundles(Constants.ARCHIVED_CONTENT_PACKAGE_BUNDLE)[0]; Bitstream bs = archived.getBitstreams()[0]; int id = bs.getID(); if (id == bitstreamID) { // remove this item from appropriate collections // (if we don't do this we get a referential integrity db error) Collection[] collections = relatedItem.getCollections(); for (Collection c : collections) { c.removeItem(relatedItem); } relatedItem.delete(); // and remove the relatedCP bitstream that links to it status = removeRelatedBitstream(context, item, bundle, relatedBitstream); } } } } return status; } /** * * Remove bitstream from wrapper object * Delete all wrapper bundles/metadata if appropriate * * @param context * @param item * @param bundle * @param bitstream * @param subInfo * @return * @throws AuthorizeException * @throws SQLException * @throws IOException */ private int removeWrapperBitstream(Context context, Item item, Bundle bundle, Bitstream bitstream, SubmissionInfo subInfo) throws AuthorizeException, SQLException, IOException { int length = removeSingleBitstream(item, bundle, bitstream); //Check if relatedCP exists Bundle[] related = item.getBundles(Constants.RELATED_CONTENT_PACKAGE_BUNDLE); String name = bundle.getName(); if (name.equals(Constants.ARCHIVED_CONTENT_PACKAGE_BUNDLE) || (name.equals("ORIGINAL") && length == 0 && related.length == 0)) { // Delete all bundles, bitstreams and metadata and reset workflow stage reached Bundle[] allBundles = item.getBundles(); for (Bundle b : allBundles) { item.removeBundle(b); item.update(); item.clearMetadata(Item.ANY, Item.ANY, Item.ANY, Item.ANY); } resetStageReached(subInfo); return ALL_BUNDLES_REMOVED; } context.commit(); item.update(); return STATUS_COMPLETE; } private int removeRelatedBitstream(Context context, Item item, Bundle bundle, Bitstream bitstream) throws AuthorizeException, SQLException, IOException { removeSingleBitstream(item, bundle, bitstream); context.commit(); //item.update(); return SINGLE_BUNDLE_REMOVED; } /** * * Removes bitstream from specified bundle * and removes bundle if empty * * @param item * @param bundle * @param bitstream * @return * @throws AuthorizeException * @throws SQLException * @throws IOException */ private int removeSingleBitstream(Item item, Bundle bundle, Bitstream bitstream) throws AuthorizeException, SQLException, IOException { bundle.removeBitstream(bitstream); int length = bundle.getBitstreams().length; if (length < 1) { item.removeBundle(bundle); item.update(); } return length; } /** * Reset the workflow stage - disables workflow submission stage buttons appropriately * @param subInfo */ private void resetStageReached(SubmissionInfo subInfo) { WorkspaceItem workspaceItem = (WorkspaceItem) subInfo.getSubmissionItem(); if (workspaceItem.getStageReached() > 2) { workspaceItem.setStageReached(2); } } /** * Process the upload of a new file! * * @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!) */ protected 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; String noPath = 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) 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; } else { // GH - virus check file if (Boolean.valueOf(ConfigurationManager.getProperty("enable.viruscheck"))){ File file = new File(request.getParameter("file")); if(!new VirusChecker( ConfigurationManager.getProperty("clamscan.path").trim(), file).isVirusFree()){ return VIRUS_CHECK_FAILED; } } // GH - end } if (subInfo != null) { // Create the bitstream Item item = subInfo.getSubmissionItem().getItem(); // do we already have a bundle? Bundle[] bundles = item.getBundles("ORIGINAL"); if (bundles.length < 1) { // set bundle's name to ORIGINAL b = item.createSingleBitstream(fileInputStream, "ORIGINAL"); } else { // we have a bundle already, just add bitstream b = bundles[0].createBitstream(fileInputStream); } // Strip all but the last filename. It would be nice // to know which OS the file came from. noPath = filePath; while (noPath.indexOf('/') > -1) { noPath = noPath.substring(noPath.indexOf('/') + 1); } while (noPath.indexOf('\\') > -1) { noPath = noPath.substring(noPath.indexOf('\\') + 1); } b.setName(noPath); b.setSource(filePath); b.setDescription(fileDescription); // Identify the format bf = FormatIdentifier.guessFormat(context, b); b.setFormat(bf); // Update to DB b.update(); item.update(); if (bf == null || !bf.isInternal()) { fileOK = true; } else { log.warn("Attempt to upload file format marked as internal system use only"); // remove bitstream from bundle.. // delete bundle if it's now empty Bundle[] bnd = b.getBundles(); bnd[0].removeBitstream(b); Bitstream[] bitstreams = bnd[0].getBitstreams(); // remove bundle if it's now empty if (bitstreams.length < 1) { item.removeBundle(bnd[0]); item.update(); } subInfo.setBitstream(null); } }// if subInfo not null else { // In any event, if we don't have the submission info, the request // was malformed return STATUS_INTEGRITY_ERROR; } // as long as everything completed ok, commit changes. Otherwise show // error page. if (fileOK) { context.commit(); // 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) { // the bitstream format is unknown! formatKnown = false; } } else { // if we get here there was a problem uploading the file! return STATUS_UPLOAD_ERROR; } }//end if attribute ends with "-path" }//end while if (!formatKnown) { /* GWaller 6/11/09 IssueID #131 This used to return STATUS_UNKNOWN_FORMAT for an unknown bitstream format. However when this happens, the Javascript controller, redisplays the upload step but lists the file uploaded at the bottom. Inorder to get to describe step (for metadata), the user must hit "Next" again. This coudl be confusing for the user esp since we still take the content even if it is of an unknown type. Better to simply log the unknown format (so we can track what the users are inputing) and return STATUS_COMPLETE so they can go to the next step as the user intended. */ log.warn("Unknown file format deposited, filename was " + noPath); return STATUS_COMPLETE; } else return STATUS_COMPLETE; } /** * 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!) */ 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 = BitstreamFormat.find(context, typeID); if (format != null) { subInfo.getBitstream().setFormat(format); } else { String userDesc = request.getParameter("format_description"); subInfo.getBitstream().setUserFormatDescription(userDesc); } // update database subInfo.getBitstream().update(); } else { return STATUS_INTEGRITY_ERROR; } return STATUS_COMPLETE; } /** * Process input from the "change file description" 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!) */ protected int processSaveFileDescription(Context context, HttpServletRequest request, HttpServletResponse response, SubmissionInfo subInfo) throws ServletException, IOException, SQLException, AuthorizeException { if (subInfo.getBitstream() != null) { subInfo.getBitstream().setDescription(request.getParameter("description")); subInfo.getBitstream().update(); context.commit(); } else { return STATUS_INTEGRITY_ERROR; } return STATUS_COMPLETE; } /***Added by CG ****/ /** * Helper method to create a new URL_BUNDLE and delete any old ones * * @param item The submission Item * @return Bundle suitable for storing a url * @throws SQLException * @throws AuthorizeException * @throws IOException */ public static Bundle getURLBundle(Item item) throws SQLException, AuthorizeException, IOException { // GWaller 25/09/09 URL_BUNDLE name now defined in Constants class Bundle[] bundles = item.getBundles(Constants.URL_BUNDLE); if ((bundles.length > 0) && (bundles[0] != null)) { item.removeBundle(bundles[0]); } return item.createBundle(Constants.URL_BUNDLE); } }