/******************************************************************************* * Australian National University Data Commons * Copyright (C) 2013 The Australian National University * * This file is part of Australian National University Data Commons. * * Australian National University Data Commons is free software: you * can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package au.edu.anu.datacommons.collectionrequest; import static java.text.MessageFormat.format; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServer; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocumentList; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import au.edu.anu.datacommons.collectionrequest.CollectionRequestStatus.ReqStatus; import au.edu.anu.datacommons.collectionrequest.PageMessages.MessageType; import au.edu.anu.datacommons.data.db.dao.CollectionRequestDAO; import au.edu.anu.datacommons.data.db.dao.CollectionRequestDAOImpl; import au.edu.anu.datacommons.data.db.dao.DropboxDAO; import au.edu.anu.datacommons.data.db.dao.DropboxDAOImpl; import au.edu.anu.datacommons.data.db.dao.GenericDAO; import au.edu.anu.datacommons.data.db.dao.GenericDAOImpl; import au.edu.anu.datacommons.data.db.dao.QuestionDAO; import au.edu.anu.datacommons.data.db.dao.QuestionDAOImpl; import au.edu.anu.datacommons.data.db.dao.QuestionMapDAO; import au.edu.anu.datacommons.data.db.dao.QuestionMapDAOImpl; import au.edu.anu.datacommons.data.db.dao.UsersDAO; import au.edu.anu.datacommons.data.db.dao.UsersDAOImpl; import au.edu.anu.datacommons.data.db.model.Domains; import au.edu.anu.datacommons.data.db.model.FedoraObject; import au.edu.anu.datacommons.data.db.model.Groups; import au.edu.anu.datacommons.data.db.model.Users; import au.edu.anu.datacommons.data.solr.SolrManager; import au.edu.anu.datacommons.data.solr.SolrUtils; import au.edu.anu.datacommons.security.CustomUser; import au.edu.anu.datacommons.security.AccessLogRecord.Operation; import au.edu.anu.datacommons.security.acl.CustomACLPermission; import au.edu.anu.datacommons.security.acl.PermissionService; import au.edu.anu.datacommons.security.service.FedoraObjectService; import au.edu.anu.datacommons.security.service.GroupService; import au.edu.anu.datacommons.storage.DcStorage; import au.edu.anu.datacommons.storage.controller.StorageController; import au.edu.anu.datacommons.storage.info.FileInfo; import au.edu.anu.datacommons.storage.info.RecordDataSummary; import au.edu.anu.datacommons.storage.provider.StorageException; import au.edu.anu.datacommons.upload.UploadService; import au.edu.anu.datacommons.util.Util; import com.sun.jersey.api.NotFoundException; import com.sun.jersey.api.view.Viewable; /** * CollectionRequestService * * Australian National University Data Commons * * This method serves REST requests related to Collection Requests. The following broad workflows are supported: * * <ol> * <li>Submitting a Collection Request that includes the specific items (datastreams) requested and the answers to * questions assigned to a collection (pid).</li> * <li>Changing the status of a Collection Request providing a reason for the change.</li> * <li>When a Collection Request is approved, providing access to the created dropbox.</li> * <li>Allow for questions to be added to the Question Bank.</li> * <li>Allow questions to be assigned to Fedora Objects so when a request is submitted, those questions must be * answered. * </ol> * * <pre> * Version Date Developer Description * 0.1 1/05/2012 Rahul Khanna (RK) Initial * 0.2 25/06/2012 Genevieve Turner (GT) Updated to filter out requests to either the logged in user or those they have review access to * 0.3 29/06/2012 Genevieve Turner (GT) Updated to use DAO and for filtering out records with permissions * 0.4 08/04/2012 Genevieve Turner (GT) Updated to allow for required and optional questions * </pre> * */ @Component @Scope("request") @Path("/collreq") public class CollectionRequestService { private static final Logger LOGGER = LoggerFactory.getLogger(CollectionRequestService.class); private static final String COLL_REQ_JSP = "/collreq.jsp"; private static final String DROPBOX_JSP = "/dropbox.jsp"; private static final String QUESTION_JSP = "/question.jsp"; private static final String DROPBOX_ACCESS_JSP = "/dropboxaccess.jsp"; @Resource(name = "dcStorage") private DcStorage dcStorage; @Resource(name = "mailSender") JavaMailSenderImpl mailSender; @Resource(name = "groupServiceImpl") GroupService groupService; @Resource(name = "fedoraObjectServiceImpl") FedoraObjectService fedoraObjectService; @Resource(name = "permissionService") private PermissionService permissionService; @Autowired protected StorageController storageController; /** * doGetAsHtml * * Australian National University Data Commons * * Gets the Collection Request page. * * <pre> * Version Date Developer Description * 0.1 1/05/2012 Rahul Khanna (RK) Initial * 0.2 25/06/2012 Genevieve Turner (GT) Updated to filter out requests to either the logged in user or those they have review access to * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * </pre> * * @return Collection Request page as HTML. */ @GET @PreAuthorize("hasRole('ROLE_REGISTERED')") @Produces(MediaType.TEXT_HTML) public Response doGetAsHtml() { Response resp = null; Map<String, Object> model = new HashMap<String, Object>(); CustomUser customUser = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Long id = customUser.getId(); List<Groups> reviewGroups = groupService.getReviewGroups(); CollectionRequestDAO collectionRequestDAO = new CollectionRequestDAOImpl(); List<CollectionRequest> collReqs = collectionRequestDAO.getPermittedRequests(id, reviewGroups); LOGGER.info("Number of collection requests: {}", collReqs.size()); model.put("collReqs", collReqs); resp = Response.ok(new Viewable(COLL_REQ_JSP, model)).build(); return resp; } /** * doGetReqItemAsHtml * * Australian National University Data Commons * * Gets the specific Collection Request with the provided ID. * * <pre> * Version Date Developer Description * 0.1 1/05/2012 Rahul Khanna (RK) Initial * 0.2 28/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * </pre> * * @param collReqId * ID of the Collection request as Long. * @return Collection Request page as HTML. */ @GET @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_REGISTERED')") @Path("{collReqId}") public Response doGetReqItemAsHtml(@PathParam("collReqId") Long collReqId) { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; LOGGER.trace("In method doGetReqItemAsHtml. Param collReqId={}.", collReqId); LOGGER.debug("Retrieving Collection Request with ID: {}...", collReqId); try { LOGGER.debug("Retrieving Collection Request with ID: {}...", collReqId); // Find the Collection Request with the specified ID. CollectionRequestDAO collectionRequestDAO = new CollectionRequestDAOImpl(); CollectionRequest collReq = collectionRequestDAO.getSingleByIdEager(collReqId); // Check if the Collection Request actually exists. If not, throw Exception. if (collReq == null) throw new Exception("Invalid Collection Request ID or no Collection Request with that ID exists."); LOGGER.debug("Found Collection Request ID {}, for Pid {}.", collReq.getId(), collReq.getPid()); model.put("collReq", collReq); // Add files in payload to model. FedoraObject fo = fedoraObjectService.getItemByPid(collReq.getPid()); if (permissionService.checkPermission(fo, CustomACLPermission.PUBLISH) || permissionService.checkPermission(fo, CustomACLPermission.REVIEW)) { FileInfo downloadables = storageController.getFileInfo(collReq.getPid(), ""); model.put("downloadables", downloadables); } } catch (Exception e) { LOGGER.error("Unable to find or retrieve Collection Request " + collReqId, e); messages.clear(); messages.add(MessageType.ERROR, e.getMessage(), model); } finally { resp = Response.ok(new Viewable(COLL_REQ_JSP, model)).build(); } return resp; } /** * doPostCollReqAsHtml * * Australian National University Data Commons * * Receives a POST request with the details of a new Collection Request to be created and creates a new Collection * Request. * * <pre> * Version Date Developer Description * 0.1 1/05/2012 Rahul Khanna (RK) Initial * 0.2 27/06/2012 Genevieve Turner (GT) Updated to associated fedoraObject * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * 0.4 08/04/2012 Genevieve Turner (GT) Updated to allow for required and optional questions * </pre> * * @param pid * Pid for which the request belongs to. * @param requestedFileSet * Datastream IDs as a set being requested. * @param allFormParams * Map of all form parameters from which questions that have been answered are extracted. * @return Response as HTML */ @POST @Produces(MediaType.TEXT_HTML) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @PreAuthorize("hasRole('ROLE_REGISTERED')") public Response doPostCollReqAsHtml(@Context HttpServletRequest request, @Context UriInfo uriInfo, @FormParam("pid") String pid, @FormParam("file") Set<String> requestedFileSet, MultivaluedMap<String, String> allFormParams) { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; UriBuilder uriBuilder = null; LOGGER.trace("In method doPostCollReqAsHtml. Param pid={}, dsIdSet={}, allFormParams={}.", new Object[] { pid, requestedFileSet, allFormParams }); // Save the Collection Request for further processing. try { Users user = new UsersDAOImpl().getUserByName(SecurityContextHolder.getContext().getAuthentication() .getName()); FedoraObject fedoraObject = fedoraObjectService.getItemByPid(pid); CollectionRequest newCollReq = new CollectionRequest(pid, user, request.getRemoteAddr(), fedoraObject); // Get a list of questions assigned to the Pid. QuestionDAO questionDAO = new QuestionDAOImpl(); List<Question> reqQuestionList = questionDAO.getQuestionsByPid(pid, true); // Iterate through the questions that need to be answered for the pid, get the answers for those questions // and add to CR. // If an answer for a question doesn't exist throw exception. for (Question iQuestion : reqQuestionList) { // Check if the answer to the current question is provided. If yes, save the answer, else throw // exception. if (allFormParams.containsKey("q" + iQuestion.getId()) && Util.isNotEmpty(allFormParams.getFirst("q" + iQuestion.getId()))) { CollectionRequestAnswer ans = new CollectionRequestAnswer(iQuestion, allFormParams.getFirst("q" + iQuestion.getId())); newCollReq.addAnswer(ans); } else { throw new Exception("All questions must be answered. The question '" + iQuestion.getQuestionText() + "' has been left blank."); } } // Iterate through the optional questions for the pid, and add the answers for those questions to the // Collection Request List<Question> optQuestionList = questionDAO.getQuestionsByPid(pid, false); for (Question iQuestion : optQuestionList) { if (allFormParams.containsKey("q" + iQuestion.getId()) && Util.isNotEmpty(allFormParams.getFirst("q" + iQuestion.getId()))) { CollectionRequestAnswer ans = new CollectionRequestAnswer(iQuestion, allFormParams.getFirst("q" + iQuestion.getId())); newCollReq.addAnswer(ans); } } LOGGER.debug("All mandatory questions answered for this Pid."); // Save the newly created CR and add success message to message set. CollectionRequestDAO requestDAO = new CollectionRequestDAOImpl(); requestDAO.create(newCollReq); uriBuilder = UriBuilder.fromPath("/collreq/").path(newCollReq.getId().toString()); // Send email to contacts of the collection. Map<String, String> varMap = new HashMap<String, String>(); varMap.put("pid", pid); varMap.put("collReqUrl", uriInfo.getBaseUriBuilder().path(this.getClass()).path(this.getClass(), "doGetReqItemAsHtml") .build(newCollReq.getId()).toString()); List<String> contactEmailList = getEmails(pid); Email email = new Email(mailSender); for (String recipientEmail : contactEmailList) email.addRecipient(recipientEmail); email.setSubject("Collection data requested"); email.setBody("mailtmpl/collreqsubmitted.txt", varMap); email.send(); messages.add(MessageType.SUCCESS, "Collection Request successfully saved. ID# " + newCollReq.getId(), model); model.put("collReq", newCollReq); uriBuilder = uriBuilder .queryParam("smsg", "Collection Request saved. ID# " + newCollReq.getId().toString()); } catch (Exception e) { LOGGER.error("Unable to create new Collection Request.", e); uriBuilder = UriBuilder.fromPath("/collreq/").queryParam("pid", pid).queryParam("emsg", e.getMessage()); } finally { resp = Response.seeOther(uriBuilder.build()).build(); } return resp; } /** * doPostUpdateCollReqAsHtml * * Australian National University Data Commons * * This method accepts changes to be made to a Collection Request. * * <pre> * Version Date Developer Description * 0.1 2/05/2012 Rahul Khanna (RK) Initial * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * </pre> * * @param collReqId * The collection ID to be updated. * @param status * The status of the collection request. E.g. active, rejected etc. * @param reason * The reason for the status change. * @return Response as HTML. */ @POST @Path("{collReqId}") @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_ANU_USER')") public Response doPostUpdateCollReqAsHtml(@PathParam("collReqId") Long collReqId, @Context UriInfo uriInfo, @FormParam("status") ReqStatus status, @FormParam("reason") String reason, @FormParam("file") Set<String> fileSet) { Response resp = null; CollectionRequest collReq = null; UriBuilder redirUri = UriBuilder.fromPath("/collreq/").path(collReqId.toString()); LOGGER.trace("In doPostUpdateCollReqAsHtml. Params collReqId={}, status={}, reason={}", new Object[] { collReqId, status, reason }); try { LOGGER.debug("Saving Collection Request ID {} with updated details...", collReqId); // Get the CR with the provided ID. CollectionRequestDAO collectionRequestDAO = new CollectionRequestDAOImpl(); collReq = collectionRequestDAO.getSingleByIdEager(collReqId); // Check if the CR exists. if (collReq == null) throw new Exception("Invalid Collection Request ID or Collection Request could not be retrieved."); // Check if a reason is provided. If not, add error to message set. if (!Util.isNotEmpty(reason)) throw new Exception("A reason must be provided for a status change."); // If the CR's current status is approved or rejected, the status cannot change anymore. ReqStatus curStatus = collReq.getLastStatus().getStatus(); if (curStatus == ReqStatus.ACCEPTED || curStatus == ReqStatus.REJECTED) throw new Exception( "Cannot change status of a CR with an Approved or Rejected status. A new CR must be submitted by the requestor for processing."); // Add a status row to the status history for that CR. Users user = new UsersDAOImpl().getUserByName(SecurityContextHolder.getContext().getAuthentication() .getName()); CollectionRequestStatus newStatus = new CollectionRequestStatus(collReq, status, reason, user); collReq.addStatus(newStatus); collReq = collectionRequestDAO.update(collReq); // Update the items requested, if required. LOGGER.debug("{} items checked.", fileSet.size()); // Add each of the items requested (datastreams) to the CR. Set<CollectionRequestItem> curItems = collReq.getItems(); // Sync the list of items in the POST with the ones already stored. for (String iFile : fileSet) { boolean isPresent = false; for (CollectionRequestItem iCurItem : curItems) { if (iCurItem.getItem().equals(iFile)) { isPresent = true; break; } } if (!isPresent) { CollectionRequestItem newItem = new CollectionRequestItem(iFile); collReq.addItem(newItem); } } GenericDAO<CollectionRequestItem, Long> collReqDao = new GenericDAOImpl<CollectionRequestItem, Long>( CollectionRequestItem.class); Iterator<CollectionRequestItem> iter = curItems.iterator(); while (iter.hasNext()) { CollectionRequestItem iItem = iter.next(); if (!fileSet.contains(iItem.getItem())) { iter.remove(); collReqDao.delete(iItem.getId()); } } // Save the updated collection request in the DB. collReq = collectionRequestDAO.update(collReq); LOGGER.debug("Updated details of CR ID# {}.", collReq.getId()); redirUri.queryParam("smsg", "Successfully added status to Status History"); // Generate the dropbox access URI. URI dropboxUri = null; if (status == ReqStatus.ACCEPTED && collReq.getItems().size() > 0) { CollectionDropbox dBox = new CollectionDropbox(collReq, user, true); collReq.setDropbox(dBox); collReq = collectionRequestDAO.update(collReq); dropboxUri = UriBuilder.fromUri(uriInfo.getBaseUri()).path(CollectionRequestService.class) .path(CollectionRequestService.class, "doGetDropboxAccessAsHtml") .queryParam("p", collReq.getDropbox().getAccessPassword()) .build(collReq.getDropbox().getAccessCode()); redirUri.queryParam( "imsg", format("Dropbox created<br /><strong>Code: </strong>{0}<br /><strong>Password: </strong>{1}", collReq.getDropbox().getAccessCode().toString(), collReq.getDropbox() .getAccessPassword())); redirUri.queryParam("imsg", format("Dropbox Access Link: <a href=''{0}''>Dropbox Access</a>", dropboxUri.toString())); } // Send out an email to the requestor. If failed, add status message advising that the requestor should be // contacted directly. try { HashMap<String, String> varMap = new HashMap<String, String>(); varMap.put("requestorGivenName", collReq.getRequestor().getGivenName()); varMap.put("collReqId", collReq.getId().toString()); varMap.put("changedByDispName", collReq.getLastStatus().getUser().getDisplayName()); varMap.put("dateChanged", collReq.getLastStatus().getTimestamp().toString()); varMap.put("status", collReq.getLastStatus().getStatus().toString()); varMap.put("reason", collReq.getLastStatus().getReason()); Email email = new Email(mailSender); email.addRecipient(collReq.getRequestor().getEmail(), collReq.getRequestor().getDisplayName()); email.setSubject("Collection Request# " + collReq.getId() + " Status Changed"); // Add a message about the dropbox being created if the status is now Accepted. if (status == ReqStatus.ACCEPTED && collReq.getItems().size() > 0) { varMap.put("dropboxLink", dropboxUri.toString()); email.setBody("mailtmpl/collreqaccepted.txt", varMap); } else { email.setBody("mailtmpl/collreqchanged.txt", varMap); } email.send(); } catch (Exception e) { redirUri.queryParam("emsg", "Could not email the requestor. Please contact him/her with the dropbox access link."); } } catch (Exception e) { LOGGER.error("Unable to add request row.", e); redirUri.queryParam("emsg", e.getMessage()); } resp = Response.seeOther(redirUri.build()).build(); return resp; } /** * doGetDropboxesAsHtml * * Australian National University Data Commons * * This method returns a list of dropboxes and their details. This method enables administration of dropboxes only. * Access to the files within the dropbox is from doGetDropboxAccessAsHtml method. * * <pre> * Version Date Developer Description * 0.1 2/05/2012 Rahul Khanna (RK) Initial * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * </pre> * * @return Response as HTML. */ @GET @Path("dropbox") @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_ANU_USER')") public Response doGetDropboxesAsHtml() { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; LOGGER.trace("In doGetDropboxesAsHtml."); try { List<CollectionDropbox> dropboxes = null; String username = SecurityContextHolder.getContext().getAuthentication().getName(); UsersDAO userDAO = new UsersDAOImpl(); Users user = userDAO.getUserByName(username); DropboxDAO dropboxDAO = new DropboxDAOImpl(); dropboxes = dropboxDAO.getUserDropboxes(user); model.put("dropboxes", dropboxes); if (dropboxes.size() == 0) messages.add(MessageType.WARNING, "No dropboxes found.", model); model.put("dropboxes", dropboxes); } catch (Exception e) { LOGGER.error("Unable to get list of Collection Dropboxes.", e); messages.clear(); messages.add(MessageType.ERROR, e.getMessage(), model); } finally { resp = Response.ok(new Viewable(DROPBOX_JSP, model)).build(); } return resp; } /** * doGetDropboxAsHtml * * Australian National University Data Commons * * This method Displays the details of a dropbox. It does not allow access to the files that have been requested in * the dropbox. * * <pre> * Version Date Developer Description * 0.1 2/05/2012 Rahul Khanna (RK) Initial * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * </pre> * * @param dropboxId * The ID of the dropbox. * * @return Response as HTML. */ @GET @Path("dropbox/{dropboxId}") @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_ANU_USER')") public Response doGetDropboxAsHtml(@PathParam("dropboxId") long dropboxId) { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; LOGGER.trace("In doGetDropboxAsHtml. Params dropboxId: {}", dropboxId); try { // Find the dropbox with the specified ID. DropboxDAO dropboxDAO = new DropboxDAOImpl(); CollectionDropbox dropbox = dropboxDAO.getSingleById(dropboxId); // Check if a valid dropbox exists and was retrieved. if (dropbox == null) throw new Exception("Invalid Dropbox ID or a dropbox with ID " + dropboxId + "doesn't exist."); model.put("dropbox", dropbox); } catch (Exception e) { LOGGER.error("Unable to get list of Collection Dropboxes.", e); messages.clear(); messages.add(MessageType.ERROR, e.getMessage(), model); } finally { resp = Response.ok(new Viewable(DROPBOX_JSP, model)).build(); } return resp; } /** * doPostUpdateDropboxAsHtml * * Australian National University Data Commons * * This method accepts POST requests to update the details of a dropbox. * * <pre> * Version Date Developer Description * 0.1 15/05/2012 Rahul Khanna (RK) Initial * 0.3 29/06/2012 Genevieve Turner (GT) Limit those authorised * </pre> * * @param dropboxId * ID of the dropbox to be updated. * @return Response as HTML. */ @POST @Path("dropbox/{dropboxId}") @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_ANU_USER')") public Response doPostUpdateDropboxAsHtml(@PathParam("dropboxId") long dropboxId) { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; // TODO Process updates to dropbox. Change notifyOnPickup and/or active status. return resp; } /** * doGetDropboxAccessAsHtml * * Australian National University Data Commons * * This method allows access to the files in a dropbox. * * <pre> * Version Date Developer Description * 0.1 2/05/2012 Rahul Khanna (RK) Initial * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * </pre> * * @param dropboxAccessCode * Access code of a dropbox. * @param password * Password to access the dropbox. * @return Response as HTML. */ @GET @Path("dropbox/access/{dropboxAccessCode}") @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_REGISTERED')") public Response doGetDropboxAccessAsHtml(@Context HttpServletRequest request, @Context UriInfo uriInfo, @PathParam("dropboxAccessCode") Long dropboxAccessCode, @QueryParam("p") String password) { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; LOGGER.trace("In doGetDropboxAccessAsHtml. Params dropboxAccessCode={}, password={}.", dropboxAccessCode, password); try { LOGGER.debug("Finding dropbox with access code {}...", dropboxAccessCode); DropboxDAO dropboxDAO = new DropboxDAOImpl(); CollectionDropbox dropbox = dropboxDAO.getSingleByAccessCode(dropboxAccessCode); if (dropbox == null) throw new NotFoundException(format("Dropbox with Access Code {0} doesn't exist.", dropboxAccessCode.toString())); LOGGER.debug("Dropbox found."); model.put("dropbox", dropbox); Users requestor = dropbox.getCollectionRequest().getRequestor(); String username = SecurityContextHolder.getContext().getAuthentication().getName(); if (!requestor.getUsername().equals(username)) throw new Exception("You are not authorised to view this dropbox."); if (new Date().after(dropbox.getExpiry())) // Check if today's date and time is after dropbox expiry. throw new Exception("This Dropbox has expired. Please submit a new Collection Request."); if (!dropbox.isActive()) // Check if dropbox active. throw new Exception("This Dropbox has been marked as inactive."); if (!Util.isNotEmpty(password)) // Check if password provided. throw new IllegalArgumentException("Please enter password for this Dropbox."); if (!password.equals(dropbox.getAccessPassword())) // Check if password correct. throw new Exception("Incorrect password entered."); // Create HashMap downloadables with a link for each item to be downloaded. HashMap<String, String> downloadables = new HashMap<String, String>(); UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(CollectionRequestService.class) .path(CollectionRequestService.class, "doGetDropboxFileAsOctetStream") .queryParam("dropboxAccessCode", dropboxAccessCode).queryParam("p", password); for (CollectionRequestItem reqItem : dropbox.getCollectionRequest().getItems()) { URI fileDlUri = uriBuilder.build(dropbox.getCollectionRequest().getPid(), reqItem.getItem()); LOGGER.debug("Adding URI {} to downloadables", fileDlUri.toString()); downloadables.put(reqItem.getItem(), fileDlUri.toString()); } model.put("downloadables", downloadables); model.put("downloadAsZipUrl", uriBuilder.build(dropbox.getCollectionRequest().getPid(), "zip")); // External references list. Collection<String> extRefs = storageController.getRecordDataSummary(dropbox.getCollectionRequest().getPid()) .getExtRefs(); if (extRefs != null && extRefs.size() > 0) { model.put("fetchables", extRefs); } // Make a log of access CollectionDropboxAccessLog log = new CollectionDropboxAccessLog(dropbox, request.getRemoteAddr()); dropbox.addAccessLogEntry(log); dropboxDAO.update(dropbox); } catch (Exception e) { LOGGER.error("Unable to get Dropbox for access.", e); messages.clear(); if (e.getClass() == IllegalArgumentException.class) messages.add(MessageType.INFO, e.getMessage(), model); else messages.add(MessageType.ERROR, e.getMessage(), model); } finally { resp = Response.ok(new Viewable(DROPBOX_ACCESS_JSP, model)).build(); } return resp; } /** * Returns the contents of a file of a group of files combined into a ZipStream as InputStream in the Response * object. The user gets a request to open or save the requested file. This method checks that the user requesting * the file has a valid collection request. * * @param pid * Pid of collection whose files * @param filename * filename of the file being requested. E.g. "data/file.txt" * @param dropboxAccessCode * Access Code of the dropbox that the requestor's been given access to * @param password * Password of dropbox * @return Response containing octet_stream of file or files as zipfile. */ @GET @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("dropbox/access/{dropboxAccessCode}/{fileInBag:.*}") public Response doGetDropboxFileAsOctetStream(@PathParam("fileInBag") String filename, @QueryParam("dropboxAccessCode") Long dropboxAccessCode, @QueryParam("p") String password) { Response resp = null; // Get dropbox requesting file. DropboxDAO dropboxDAO = new DropboxDAOImpl(); CollectionDropbox dropbox = dropboxDAO.getSingleByAccessCode(dropboxAccessCode); Users requestor = dropbox.getCollectionRequest().getRequestor(); String username = SecurityContextHolder.getContext().getAuthentication().getName(); String pid = dropbox.getCollectionRequest().getPid(); LOGGER.trace("User {} requested dropbox for pid: {}, filename: {}", username, pid, filename); // If dropbox is valid and the requestor of the collection request is the one accessing it, return file as octet // stream. try { if (dropbox.isValid(password) && requestor.getUsername().equals(username)) { LOGGER.info("Dropbox details valid. ID: {}, Access Code: {}. Returning file requested.", dropbox .getId().toString(), dropbox.getAccessCode().toString()); Set<CollectionRequestItem> items = dropbox.getCollectionRequest().getItems(); if (filename.equalsIgnoreCase("zip")) { Set<String> fileSet = new HashSet<String>(); for (CollectionRequestItem item : items) { fileSet.add(item.getItem()); } resp = getBagFilesAsZip(pid, fileSet, DcStorage.convertToDiskSafe(pid) + ".zip"); } else { boolean isAllowedItem = false; for (CollectionRequestItem item : items) { if (item.getItem().equals(filename)) { isAllowedItem = true; break; } } if (isAllowedItem) { // addAccessLog(Operation.READ); resp = getBagFileOctetStreamResp(pid, filename); } else { resp = Response.status(Status.FORBIDDEN).build(); } } } else { LOGGER.warn("Unauthorised access to Dropbox ID: {}, Access Code: {}. Returning HTTP 403 Forbidden.", dropbox.getId().toString(), dropbox.getAccessCode().toString()); resp = Response.status(Status.FORBIDDEN).build(); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); resp = Response.serverError().entity(e.getMessage()).build(); } return resp; } /** * doGetQuestionsAsHtml * * Australian National University Data Commons * * Returns a Viewable with all questions in the question bank in its model. * * <pre> * Version Date Developer Description * 0.1 2/05/2012 Rahul Khanna (RK) Initial * 0.3 29/06/2012 Genevieve Turner (GT) Limit those authorised * </pre> * * @return Response as HTML. */ @GET @Path("question") @Produces(MediaType.TEXT_HTML) @PreAuthorize("hasRole('ROLE_ANU_USER')") public Response doGetQuestionsAsHtml() { PageMessages messages = new PageMessages(); Map<String, Object> model = new HashMap<String, Object>(); Response resp = null; LOGGER.info("User {} requested the questions page.", getCurUsername()); try { // Get all questions from the Question Bank. List<Question> questions = getAllQuestions(); LOGGER.debug("Retrieved {} questions from question bank.", questions.size()); // Add a warning to the message set to let the user know that there aren't any questions in the question // bank. if (questions.size() == 0) { messages.add(MessageType.WARNING, "No questions found in the question bank.", model); } model.put("questions", questions); List<Groups> groups = groupService.getReviewGroups(); model.put("groups", groups); } catch (Exception e) { LOGGER.error("Unable to retrieve questions from question bank", e); messages.clear(); messages.add(MessageType.ERROR, e.getMessage(), model); } finally { resp = Response.ok(new Viewable(QUESTION_JSP, model)).build(); } return resp; } /** * doPostQuestionAsHtml * * Australian National University Data Commons * * Accepts POST requests to add new questions in the question bank as well as requests to assign a question to a * Pid. * * <pre> * Version Date Developer Description * 0.1 2/05/2012 Rahul Khanna (RK) Initial * 0.2 29/06/2012 Genevieve Turner (GT) Updated to use the DAO pattern and limit those authorised * 0.4 04/04/2012 Genevieve Turner (GT) Updated to allow for questions against groups and domains * </pre> * * @param submit * The task to be performed. Value comes from the submit button in a form. "Add Question" to add a * question to the question bank, or "Save" to update the list of questions against a Pid. * @param questionText * Question as String * @param pid * Pid as String * @param qIdSet * Questions to be assigned to Pid as Set. * @return Response as HTML. */ @POST @Path("question") @Produces(MediaType.TEXT_HTML) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @PreAuthorize("hasRole('ROLE_ANU_USER')") public Response doPostQuestionAsHtml(@Context UriInfo uriInfo, @FormParam("submit") String submit, @FormParam("q") String questionText, @FormParam("pid") String pid, @FormParam("qid") Set<Long> qIdSet, @FormParam("group") Long groupId, @FormParam("domain") Long domainId, @FormParam("idPidQ") Set<Long> requiredQuestions, @FormParam("optQid") Set<Long> optQidSet) { LOGGER.debug("Number of req q's: {}, Number of opt q's: {}", qIdSet.size(), optQidSet.size()); Response resp = null; UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(CollectionRequestService.class) .path(CollectionRequestService.class, "doGetQuestionsAsHtml"); LOGGER.trace("In doPostQuestionAsHtml. Params submit={}, questionStr={}, pid={}, qid={}.", new Object[] { submit, questionText, pid, qIdSet }); if (Util.isNotEmpty(pid)) uriBuilder = uriBuilder.queryParam("pid", pid); if (Util.isNotEmpty(submit)) { // Adding a question to the question bank. if (submit.equals("Add Question")) { try { // Validate question text. questionText = questionText.trim(); if (questionText.equals("")) throw new Exception("Question text not provided. A Question cannot be blank."); LOGGER.debug("Saving question in question bank...", pid); // Create Question object and persist it. Question question = new Question(questionText); QuestionDAO questionDAO = new QuestionDAOImpl(); questionDAO.create(question); uriBuilder = uriBuilder.queryParam("smsg", "The question <em>" + question.getQuestionText() + "</em> saved in the Question Bank."); LOGGER.info("Saved question in question bank: {}", question.getQuestionText()); } catch (Exception e) { LOGGER.error("Unable to save question in the question bank.", e); uriBuilder.queryParam("emsg", "Unable to save question in the question bank."); } resp = Response.seeOther(uriBuilder.build()).build(); } // Assigning questions to a pid. else if (submit.equals("Save")) { try { QuestionDAO questionDAO = new QuestionDAOImpl(); QuestionMapDAO questionMapDAO = new QuestionMapDAOImpl(); updateQuestions(questionDAO, questionMapDAO, qIdSet, pid, groupId, domainId, Boolean.TRUE); updateQuestions(questionDAO, questionMapDAO, optQidSet, pid, groupId, domainId, Boolean.FALSE); uriBuilder = uriBuilder.queryParam("smsg", "Question List updated for this Item."); } catch (Exception e) { LOGGER.error("Unable to update questions for Pid " + pid, e); uriBuilder = uriBuilder.queryParam("emsg", "Unable to update questions for this Item."); } resp = Response.seeOther(uriBuilder.build()).build(); } } else { LOGGER.error("Attempt to POST question without param 'submit'."); resp = Response.serverError().build(); } return resp; } /** * doGetDsListAsJson * * Australian National University Data Commons * * This method returns JSON responses for requests for the following: * * <ol> * <li>List of Datastreams of a Fedora Object.</li> * <li>List of Collection Requests and their details</li> * <ol> * * <pre> * Version Date Developer Description * 0.1 8/05/2012 Rahul Khanna (RK) Initial * 0.3 27/06/2012 Genevieve Turner (GT) Updated to use DAO pattern and limit those authorised * 0.4 04/04/2012 Genevieve Turner (GT) Updated to allow for questions against groups and domains and to allow for optional and required questions * </pre> * * @return JSON object */ @GET @Produces(MediaType.APPLICATION_JSON) @Path("json") @PreAuthorize("hasRole('ROLE_REGISTERED')") public Response doGetCollReqInfoAsJson(@QueryParam("task") String task, @QueryParam("pid") String pid, @QueryParam("group") Long groupId, @QueryParam("domain") Long domainId) { Response resp = null; LOGGER.trace("In doGetDsListAsJson. Params task={}, pid={}.", task, pid); // Gets a list of Questions assigned to a Pid. if (task.equals("listPidQuestions")) { try { QuestionDAO questionDAO = new QuestionDAOImpl(); List<Question> reqQuestions = questionDAO.getQuestionsByPid(pid, true); LOGGER.info("Number of req questions: {}", reqQuestions.size()); List<Question> optQuestions = questionDAO.getQuestionsByPid(pid, false); LOGGER.info("Number of opt questions: {}", optQuestions.size()); resp = processQuestionsJsonResponse(reqQuestions, optQuestions); } catch (Exception e) { LOGGER.error("Unable to get list of questions for Pid " + pid, e); resp = Response.serverError().build(); } } else if ("listGroupQuestions".equals(task)) { try { QuestionDAO questionDAO = new QuestionDAOImpl(); List<Question> reqQuestions = questionDAO.getQuestionsByGroup(groupId, true); List<Question> optQuestions = questionDAO.getQuestionsByGroup(groupId, false); resp = processQuestionsJsonResponse(reqQuestions, optQuestions); } catch (Exception e) { LOGGER.error("Unable to get list of questions for group id ", groupId, e); resp = Response.serverError().build(); } } else if ("listDomainQuestions".equals(task)) { try { QuestionDAO questionDAO = new QuestionDAOImpl(); List<Question> reqQuestions = questionDAO.getQuestionsByDomain(domainId, true); List<Question> optQuestions = questionDAO.getQuestionsByDomain(domainId, false); resp = processQuestionsJsonResponse(reqQuestions, optQuestions); } catch (Exception e) { LOGGER.error("Unable to get list of questions for group id ", groupId, e); resp = Response.serverError().build(); } } // Get a list of Collection Requests and their details. else if (task.equals("listCollReq")) { JSONArray reqStatusListJsonArray = new JSONArray(); try { LOGGER.debug("Requested Collection Requests as JSON."); // Retrieve Collection Requests. CustomUser customUser = (CustomUser) SecurityContextHolder.getContext().getAuthentication() .getPrincipal(); Long id = customUser.getId(); List<Groups> reviewGroups = groupService.getReviewGroups(); CollectionRequestDAO requestDAO = new CollectionRequestDAOImpl(); List<CollectionRequest> reqStatusList = requestDAO.getPermittedRequests(id, reviewGroups); // Add the details of each CR into a JSONObject. Then add that JSONObject to a JSONArray. for (CollectionRequest iCr : reqStatusList) { JSONObject reqStatusJsonObj = new JSONObject(); reqStatusJsonObj.put("id", iCr.getId()); reqStatusJsonObj.put("pid", iCr.getPid()); reqStatusJsonObj.put("requestor", iCr.getRequestor().getUsername()); reqStatusJsonObj.put("timestamp", iCr.getTimestamp().toString()); reqStatusJsonObj.put("lastStatus", iCr.getLastStatus().getStatus().toString()); reqStatusJsonObj.put("lastStatusTimestamp", iCr.getLastStatus().getTimestamp().toString()); reqStatusListJsonArray.put(reqStatusJsonObj); } // Convert the JSONArray into a JSON String and include in Response object. resp = Response.ok(reqStatusListJsonArray.toString(), MediaType.APPLICATION_JSON_TYPE).build(); } catch (Exception e) { LOGGER.error("Unable to get list of Collection Requests."); resp = Response.serverError().build(); } } return resp; } /** * Creates a Response object containing the contents of a single file in a bag of collection as Response object * containing InputStream. * * @param pid * Pid of the collection from which a bagfile is to be read. * @param fileInBag * Name of file in bag whose contents are to be returned as InputStream. * @return Response object including HTTP headers and InputStream containing file contents. * @throws IOException */ protected Response getBagFileOctetStreamResp(String pid, String fileInBag) throws IOException, StorageException { Response resp = null; InputStream is = null; if (!storageController.fileExists(pid, fileInBag)) { throw new NotFoundException(format("File {0} not found in record {1}", fileInBag, pid)); } is = storageController.getFileStream(pid, fileInBag); ResponseBuilder respBuilder = Response.ok(is, MediaType.APPLICATION_OCTET_STREAM_TYPE); // Add filename, MD5 and file size to response header. FileInfo fileInfo = storageController.getFileInfo(pid, fileInBag); respBuilder = respBuilder.header("Content-Disposition", format("attachment; filename=\"{0}\"", fileInfo.getFilename())); String md5 = fileInfo.getMessageDigests().get("MD5"); if (md5 != null && md5.length() > 0) { respBuilder = respBuilder.header("Content-MD5", md5); } respBuilder = respBuilder.header("Content-Length", Long.toString(fileInfo.getSize(), 10)); respBuilder = respBuilder.lastModified(new Date(fileInfo.getLastModified().toMillis())); resp = respBuilder.build(); return resp; } /** * Creates a Response object containing a Zip file comprised of data from multiple files in a bag of a collection. * * @param pid * Pid of the collection whose files are to be included in the Response object. * * @param fileSet * Set of file names as Set<String> that are to be included in the Response. * @param zipFilename * Name of the zip file that will be added to the Content-Disposition HTTP header. * @return Response object */ private Response getBagFilesAsZip(String pid, Set<String> fileSet, String zipFilename) { Response resp = null; InputStream zipStream; try { zipStream = storageController.createZipStream(pid, fileSet); ResponseBuilder respBuilder = Response.ok(zipStream, MediaType.APPLICATION_OCTET_STREAM_TYPE); respBuilder.header("Content-Disposition", format("attachment; filename=\"{0}\"", zipFilename)); resp = respBuilder.build(); } catch (IOException | StorageException e) { LOGGER.error(e.getMessage(), e); throw new NotFoundException(e.getMessage()); } return resp; } /** * Save the questions to the database * * @param questionDAO * The question DAO * @param questionMapDAO * The question map DAO * @param questions * The list of questions to check * @param pid * The pid to assign the values to * @param groupId * The group to assign the questions to * @param domainId * The domain to assign the questions to * @param required * Whether set of questions are required questions or not */ private void updateQuestions(QuestionDAO questionDAO, QuestionMapDAO questionMapDAO, Set<Long> questions, String pid, Long groupId, Long domainId, Boolean required) { List<Question> curQuestions = questionDAO.getQuestionsForObject(pid, groupId, domainId, required); for (Long iUpdatedId : questions) { boolean isAlreadyMapped = false; for (Question iCurQuestion : curQuestions) { if (iCurQuestion.getId() == iUpdatedId.longValue()) { isAlreadyMapped = true; break; } } if (!isAlreadyMapped) { Question question = questionDAO.getSingleById(iUpdatedId); LOGGER.debug("Adding Question '{}' against Pid {}", question.getQuestionText(), pid); QuestionMap qm = null; // Create the question map for the pid, group or domain if (pid != null && pid.trim().length() > 0) { qm = new QuestionMap(pid, question, required); } else if (groupId != null) { GenericDAO<Groups, Long> genericDAO = new GenericDAOImpl<Groups, Long>(Groups.class); Groups group = (Groups) genericDAO.getSingleById(groupId); qm = new QuestionMap(group, question, required); } else if (domainId != null) { GenericDAO<Domains, Long> genericDAO = new GenericDAOImpl<Domains, Long>(Domains.class); Domains domain = (Domains) genericDAO.getSingleById(domainId); qm = new QuestionMap(domain, question, required); } if (qm != null) { questionMapDAO.create(qm); } } } // Check if each question for a pid, group, or domain is provided in the updated list. If not, delete it. for (Question iCurQuestion : curQuestions) { if (!questions.contains(iCurQuestion.getId())) { LOGGER.debug("Mapping of Question ID" + iCurQuestion.getId() + "to be deleted..."); QuestionMap questionMap = questionMapDAO.getSingleByObjectAndQuestion(iCurQuestion, pid, groupId, domainId); questionMapDAO.delete(questionMap.getId()); } } } /** * getAllQuestions * * Australian National University Data Commons * * Gets a list of all questions from the Question Bank. * * <pre> * Version Date Developer Description * 0.1 16/05/2012 Rahul Khanna (RK) Initial * </pre> * * @return Questions as List<Questions> */ private List<Question> getAllQuestions() { List<Question> questions = new QuestionDAOImpl().getAll(); return questions; } private Response processQuestionsJsonResponse(List<Question> reqQuestions, List<Question> optQuestions) throws JSONException { JSONObject questionsJson = new JSONObject(); JSONObject reqQuestionsJson = new JSONObject(); // Add the Id and question (String) for each Question (Object) into a JSONObject. for (Question iQuestion : reqQuestions) { reqQuestionsJson.put(iQuestion.getId().toString(), iQuestion.getQuestionText()); } questionsJson.put("required", reqQuestionsJson); JSONObject optQuestionsJson = new JSONObject(); for (Question iQuestion : optQuestions) { optQuestionsJson.put(iQuestion.getId().toString(), iQuestion.getQuestionText()); } questionsJson.put("optional", optQuestionsJson); // Convert the JSONObject into a JSON String and include it in the Response object. return Response.ok(questionsJson.toString(), MediaType.APPLICATION_JSON_TYPE).build(); } private List<String> getEmails(String pid) { List<String> emailList = new ArrayList<String>(); SolrServer solrServer = SolrManager.getInstance().getSolrServer(); SolrQuery solrQuery = new SolrQuery(); solrQuery.setQuery(format("id:\"{0}\"", SolrUtils.escapeSpecialCharacters(pid))); solrQuery.addField("id"); solrQuery.addField("unpublished.email"); QueryResponse queryResponse; try { queryResponse = solrServer.query(solrQuery); SolrDocumentList resultList = queryResponse.getResults(); if (resultList.getNumFound() == 0) throw new IllegalArgumentException(format("A collection doesn't exist with the Pid {0}", pid)); if (resultList.getNumFound() > 1) throw new IllegalArgumentException(format("Multiple collections found with Pid {0}", pid)); if (resultList.get(0).getFieldValues("unpublished.email") != null) for (Object emailAsObj : resultList.get(0).getFieldValues("unpublished.email")) emailList.add((String) emailAsObj); } catch (SolrServerException e) { LOGGER.warn(format("Unable to execute Solr Query to retrieve emails for pid {0}.", pid), e); } return emailList; } private String getCurUsername() { return SecurityContextHolder.getContext().getAuthentication().getName(); } }