CollectionController.java
package org.xandercat.pmdb.controller;
import java.io.IOException;
import java.security.Principal;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.xandercat.pmdb.dto.CollectionPermission;
import org.xandercat.pmdb.dto.Movie;
import org.xandercat.pmdb.dto.MovieCollection;
import org.xandercat.pmdb.dto.PmdbUser;
import org.xandercat.pmdb.exception.WebServicesException;
import org.xandercat.pmdb.exception.CollectionSharingException;
import org.xandercat.pmdb.form.collection.CollectionForm;
import org.xandercat.pmdb.form.collection.ExportForm;
import org.xandercat.pmdb.form.collection.ExportType;
import org.xandercat.pmdb.form.collection.ImportForm;
import org.xandercat.pmdb.form.collection.ImportOptionsForm;
import org.xandercat.pmdb.form.collection.ShareCollectionForm;
import org.xandercat.pmdb.service.CollectionService;
import org.xandercat.pmdb.service.MovieService;
import org.xandercat.pmdb.service.UserService;
import org.xandercat.pmdb.util.Alerts;
import org.xandercat.pmdb.util.ExcelPorter;
import org.xandercat.pmdb.util.ViewUtil;
/**
* Controller for functions specific to movie collections management.
*
* @author Scott Arnold
*/
@Controller
public class CollectionController {
private static final Logger LOGGER = LogManager.getLogger(CollectionController.class);
@Autowired
private UserService userService;
@Autowired
private CollectionService collectionService;
@Autowired
private MovieService movieService;
@ModelAttribute("viewTab")
public String getViewTab() {
return ViewUtil.TAB_COLLECTIONS;
}
/**
* Page for showing viewable movie collections and actions that can be taken for them.
*
* @param model model
* @param principal principal
*
* @return page for showing viewable movie collections and actions that can be taken for them
*/
@RequestMapping("/collections")
public String collections(Model model, Principal principal) {
String username = principal.getName();
Optional<MovieCollection> defaultMovieCollection = collectionService.getDefaultMovieCollection(username);
if (defaultMovieCollection.isPresent()) {
model.addAttribute("defaultMovieCollection", defaultMovieCollection.get());
}
List<MovieCollection> movieCollections = collectionService.getViewableMovieCollections(username);
List<MovieCollection> shareOffers = collectionService.getShareOfferMovieCollections(username);
model.addAttribute("movieCollections", movieCollections);
if (shareOffers.size() > 0) {
model.addAttribute("shareOffers", shareOffers);
}
return "collection/collections";
}
/**
* Page for adding a new movie collection.
*
* @param model model
*
* @return page for adding a new movie collection
*/
@RequestMapping("/collections/addCollection")
public String addCollection(Model model) {
model.addAttribute("collectionForm", new CollectionForm());
return "collection/addCollection";
}
/**
* Process adding a new movie collection. If it is the user's first movie collection,
* automatically set it as the default/active collection for the user and take them to
* the home page (movie list) for that collection. If it is not the user's first movie
* collection, just return them to the movie collections management page.
*
* @param model model
* @param principal principal
* @param collectionForm movie collection form
* @param result binding result
*
* @return page following adding a new movie collection.
*/
@RequestMapping("/collections/addCollectionSubmit")
public String addCollectionSubmit(Model model, Principal principal,
@ModelAttribute("collectionForm") @Valid CollectionForm collectionForm,
BindingResult result) {
if (result.hasErrors()) {
return "collection/addCollection";
}
List<MovieCollection> movieCollections = collectionService.getViewableMovieCollections(principal.getName());
MovieCollection movieCollection = new MovieCollection();
movieCollection.setName(collectionForm.getName());
movieCollection.setCloud(collectionForm.isCloud());
try {
collectionService.addMovieCollection(movieCollection, principal.getName());
} catch (WebServicesException e1) {
LOGGER.error("Unable to save movie collection to cloud.", e1);
Alerts.setErrorMessage(model, "Movie collection could not be added to the cloud.");
return "collection/addCollection";
}
if (movieCollections.size() == 0) {
// user had no viewable movie collections; set the newly created collection as default
try {
collectionService.setDefaultMovieCollection(movieCollection.getId(), principal.getName());
return "redirect:/"; // after setting a new default collection, immediately go to movie list for that collection
} catch (CollectionSharingException e) {
LOGGER.error("Unable to set newly created movie collection as default. This shouldn't happen.", e);
// despite that this represents an unsettling error, there is no real value in notifying the user, so not setting message here
}
}
Alerts.setMessage(model, "Movie collection added.");
return collections(model, principal);
}
/**
* Change the default/active collection for the user to the collection of given id and return them to the
* home page (movie list) for that collection.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to change to
*
* @return home page for the new default/active collection
*/
@RequestMapping("/collections/changeDefaultCollection")
public String changeDefaultCollection(Model model, Principal principal, @RequestParam String collectionId) {
try {
collectionService.setDefaultMovieCollection(collectionId, principal.getName());
return "redirect:/"; // after setting a new default collection, immediately go to movie list for that collection
} catch (CollectionSharingException e) {
LOGGER.error("Unable to set default movie collection", e);
Alerts.setErrorMessage(model, "Default movie collection could not be set.");
return collections(model, principal);
}
}
/**
* Page for editing a movie collection.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to edit
*
* @return page for editing movie collection of given id
*/
@RequestMapping("/collections/editCollection")
public String editCollection(Model model, Principal principal, @RequestParam String collectionId) {
MovieCollection movieCollection = null;
try {
movieCollection = collectionService.getViewableMovieCollection(collectionId, principal.getName());
} catch (CollectionSharingException e) {
LOGGER.error("Unable to edit movie collection.", e);
Alerts.setErrorMessage(model, "Unable to edit requested movie collection.");
return collections(model, principal);
}
CollectionForm collectionForm = new CollectionForm(movieCollection);
model.addAttribute("collectionForm", collectionForm);
return "collection/editCollection";
}
/**
* Process editing of a movie collection and return to collections management.
*
* @param model model
* @param principal principal
* @param collectionForm movie collection form
* @param result binding result
*
* @return collections management page
*/
@RequestMapping("/collections/editCollectionSubmit")
public String editCollectionSubmit(Model model, Principal principal,
@ModelAttribute("collectionForm") @Valid CollectionForm collectionForm,
BindingResult result) {
if (result.hasErrors()) {
return "collection/editCollection";
}
MovieCollection movieCollection = new MovieCollection();
movieCollection.setId(collectionForm.getId());
movieCollection.setName(collectionForm.getName());
movieCollection.setCloud(collectionForm.isCloud()); // this isn't really used, as it will only update the name
try {
collectionService.updateMovieCollection(movieCollection, principal.getName());
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to save collection.", e);
Alerts.setErrorMessage(model, "Unable to save movie collection.");
return collections(model, principal);
}
Alerts.setMessage(model, "Movie collection saved.");
return collections(model, principal);
}
/**
* Delete movie collection of given id and return to collections management. There is no intermediate
* confirmation page; confirmation should be handled on client side.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to delete
*
* @return movie collections management page
*/
@RequestMapping(value="/collections/deleteCollection", method=RequestMethod.POST)
public String deleteCollection(Model model, Principal principal, @RequestParam String collectionId) {
try {
collectionService.deleteMovieCollection(collectionId, principal.getName());
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to delete collection.", e);
Alerts.setErrorMessage(model, "Unable to delete movie collection.");
return collections(model, principal);
}
Alerts.setMessage(model, "Movie collection deleted.");
return collections(model, principal);
}
/**
* Page for changing who a movie collection is shared with.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to edit sharing of
*
* @return page for changing who a movie collection is shared with
*/
@RequestMapping("/collections/editSharing")
public String editSharing(Model model, Principal principal, @RequestParam String collectionId) {
try {
MovieCollection movieCollection = collectionService.getViewableMovieCollection(collectionId, principal.getName());
List<CollectionPermission> collectionPermissions = collectionService.getCollectionPermissions(collectionId, principal.getName());
model.addAttribute("movieCollection", movieCollection);
model.addAttribute("collectionPermissions", collectionPermissions);
} catch (CollectionSharingException e) {
LOGGER.error("Unable to retrieve collection permissions for collection " + collectionId + " user " + principal.getName(), e);
Alerts.setErrorMessage(model, "Sharing is not available on the collection.");
return collections(model, principal);
}
return "collection/editSharing";
}
/**
* Accept a movie collection share offer and return to collections management.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to accept share offer of
* @param session session
*
* @return collections management page
*/
@RequestMapping("/collections/acceptShareOffer")
public String acceptShareOffer(Model model, Principal principal, @RequestParam String collectionId, HttpSession session) {
try {
collectionService.acceptShareOffer(collectionId, principal.getName());
ViewUtil.updateNumShareOffers(collectionService, session, principal.getName());
Alerts.setMessage(model, "Share offer accepted.");
} catch (CollectionSharingException e) {
Alerts.setErrorMessage(model, "Unable to accept share offer.");
}
return collections(model, principal);
}
/**
* Decline a movie collection share offer and return to collections management.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to decline share offer of
* @param session session
*
* @return collections management page
*/
@RequestMapping("/collections/declineShareOffer")
public String declineShareOffer(Model model, Principal principal, @RequestParam String collectionId, HttpSession session) {
try {
collectionService.declineShareOffer(collectionId, principal.getName());
ViewUtil.updateNumShareOffers(collectionService, session, principal.getName());
Alerts.setMessage(model, "Share offer declined.");
} catch (CollectionSharingException e) {
Alerts.setErrorMessage(model, "Unable to decline share offer.");
}
return collections(model, principal);
}
/**
* Toggle whether or not a user can edit the shared movie collection.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to toggle edit permission on
* @param username username of user to toggle edit permission for
*
* @return edit sharing page
*/
@RequestMapping("/collections/toggleEditPermission")
public String toggleEditPermission(Model model, Principal principal, @RequestParam String collectionId, @RequestParam String username) {
try {
Optional<CollectionPermission> permission = collectionService.getCollectionPermission(collectionId, username, principal.getName());
if (!permission.isPresent()) {
throw new CollectionSharingException("Unable to obtain permission for collection " + collectionId + " user " + username);
}
collectionService.updateEditable(collectionId, username, !permission.get().isAllowEdit(), principal.getName());
Alerts.setMessage(model, "Edit permission updated.");
} catch (CollectionSharingException e) {
LOGGER.error("Unable to update edit permission for user.", e);
Alerts.setErrorMessage(model, "Could not update edit permission for user.");
}
return editSharing(model, principal, collectionId);
}
/**
* Revoke share of movie collection with user.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to revoke share permission to
* @param username username of user to revoke share permission from
*
* @return edit sharing page
*/
@RequestMapping(value="/collections/revokePermission", method=RequestMethod.POST)
public String revokePermission(Model model, Principal principal, @RequestParam String collectionId, @RequestParam String username) {
try {
collectionService.unshareMovieCollection(collectionId, username, principal.getName());
Alerts.setMessage(model, "Share revoked.");
} catch (CollectionSharingException e) {
LOGGER.error("Unable to revoke share.", e);
Alerts.setErrorMessage(model, "Share could not be revoked.");
}
return editSharing(model, principal, collectionId);
}
/**
* Revoke share of movie collection on self. This is for users who no longer want access to a movie collection
* that was previously shared with them.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection that share is no longer desired of
*
* @return collections management page
*/
@RequestMapping(value="/collections/revokeMyPermission", method=RequestMethod.POST)
public String revokeMyPermission(Model model, Principal principal, @RequestParam String collectionId) {
try {
collectionService.unshareMovieCollection(collectionId, principal.getName(), principal.getName());
Alerts.setMessage(model, "Removed access to collection.");
} catch (CollectionSharingException e) {
LOGGER.error("User unable to remove their own permission to a collection.", e);
Alerts.setErrorMessage(model, "Access cannot be removed.");
}
return collections(model, principal);
}
/**
* Page for sharing a movie collection with other users.
*
* @param model model
* @param principal principal
* @param collectionId id of movie collection to update sharing on
*
* @return page for sharing a movie collection with other users.
*/
@RequestMapping("/collections/shareCollection")
public String shareCollection(Model model, Principal principal, @RequestParam String collectionId) {
try {
MovieCollection movieCollection = collectionService.getViewableMovieCollection(collectionId, principal.getName());
model.addAttribute("movieCollection", movieCollection);
} catch (CollectionSharingException e) {
LOGGER.error("Unable to share movie collection.", e);
Alerts.setErrorMessage(model, "Unable to share movie collection.");
return editSharing(model, principal, collectionId);
}
model.addAttribute("shareCollectionForm", new ShareCollectionForm(collectionId));
return "collection/shareCollection";
}
/**
* Process sharing of movie collection with another user.
*
* This is a low security process intended for a generally trusted user base. With a less
* trusted user base, the user would not be notified if the user reference was invalid, and users would have some measure of
* control over who could share collections with them.
*
* @param model model
* @param principal principal
* @param shareCollectionForm form for sharing collection with another user
* @param result binding result
*
* @return edit sharing page
*/
@RequestMapping("/collections/shareCollectionSubmit")
public String shareCollectionSubmit(Model model, Principal principal,
@ModelAttribute("shareCollectionForm") @Valid ShareCollectionForm shareCollectionForm,
BindingResult result) {
Optional<PmdbUser> shareWithUser = userService.getUser(shareCollectionForm.getUsernameOrEmail());
if (!shareWithUser.isPresent()) {
shareWithUser = userService.getUserByEmail(shareCollectionForm.getUsernameOrEmail());
}
if (!shareWithUser.isPresent()) {
//TODO: Future educational activity -- see about using SpringConstraintValidationFactory to create a Spring wired validator for this
result.rejectValue("usernameOrEmail", "{validation.UserReference.message}", "Invalid user reference.");
}
if (result.hasErrors()) {
MovieCollection movieCollection = null;
try {
movieCollection = collectionService.getViewableMovieCollection(shareCollectionForm.getCollectionId(), principal.getName());
} catch (CollectionSharingException e) {
}
model.addAttribute("movieCollection", movieCollection);
return "collection/shareCollection";
}
try {
collectionService.shareMovieCollection(shareCollectionForm.getCollectionId(),
shareWithUser.get().getUsername(), shareCollectionForm.isEditable(), principal.getName());
Alerts.setMessage(model, "Offer sent to share collection.");
} catch (CollectionSharingException e) {
LOGGER.error("Unable to share collection.", e);
Alerts.setErrorMessage(model, "Unable to share collection.");
}
return editSharing(model, principal, shareCollectionForm.getCollectionId());
}
/**
* Page for exporting a movie collection.
*
* @param model model
* @param principal principal
*
* @return collections export page
*/
@RequestMapping("/collections/export")
public String export(Model model, Principal principal) {
List<MovieCollection> movieCollections = collectionService.getViewableMovieCollections(principal.getName());
if (movieCollections.size() == 0) {
Alerts.setErrorMessage(model, "There are no collections that can be exported.");
return collections(model, principal);
}
Optional<MovieCollection> defaultMovieCollection = collectionService.getDefaultMovieCollection(principal.getName());
String exportCollectionId = movieCollections.get(0).getId();
if (defaultMovieCollection.isPresent()) {
exportCollectionId = defaultMovieCollection.get().getId();
}
model.addAttribute("exportForm", new ExportForm(exportCollectionId, ExportType.XLSX));
model.addAttribute("collectionOptions", ViewUtil.getOptions(movieCollections, "getId", "getName"));
model.addAttribute("typeOptions", ViewUtil.getOptions(ExportType.class));
return "collection/export";
}
/**
* Export a movie collection.
*
* @param model model
* @param principal principal
* @param exportForm form for exporting a movie collection
* @param response HTTP servlet response
*
* @return export page
*/
@RequestMapping(value="/collections/exportSubmit", method=RequestMethod.POST)
public String exportSubmit(Model model, Principal principal,
@ModelAttribute("exportForm") ExportForm exportForm, HttpServletResponse response) {
if (exportForm.getCollections() == null || exportForm.getCollections().size() == 0) {
Alerts.setErrorMessage(model, "You must select at least 1 collection to export.");
return export(model, principal);
}
try {
ExportType exportType = ExportType.valueOf(exportForm.getType());
ExcelPorter.Format format = ExcelPorter.Format.XLSX;
if (exportType == ExportType.XLS) {
format = ExcelPorter.Format.XLS;
}
ExcelPorter excelPorter = new ExcelPorter(format);
response.setContentType(excelPorter.getContentType());
response.setHeader("Content-Disposition", "attachment; filename=\"" + excelPorter.getFilename("PMDBExport") + "\"");
List<String> collectionIds = exportForm.getCollections();
for (String collectionId : collectionIds) {
MovieCollection movieCollection = collectionService.getViewableMovieCollection(collectionId, principal.getName());
Set<Movie> movies = movieService.getMoviesForCollection(collectionId, principal.getName());
List<String> attributeKeys = movieService.getAttributeKeysForCollection(collectionId, principal.getName());
excelPorter.addSheet(movieCollection, movies, attributeKeys);
}
excelPorter.writeWorkbook(response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
LOGGER.error("Unable to export collections to Excel.", e);
}
return export(model, principal);
}
/**
* Page for importing movie collections.
*
* @param model model
* @param principal principal
*
* @return page for importing movie collections.
*/
@RequestMapping("/collections/import")
public String importCollections(Model model, Principal principal) {
model.addAttribute("importForm", new ImportForm());
return "collection/import";
}
/**
* Import a movie collection. This does not fully import the movie collection; it performs an initial
* analysis of the import in order to provide a series of import options before completing the import process.
*
* @param model model
* @param principal principal
* @param session session
* @param importForm form for importing a movie
*
* @return import options page
*/
@RequestMapping("/collections/importSubmit")
public String importCollectionsSubmit(Model model, Principal principal, HttpSession session,
@ModelAttribute("importForm") ImportForm importForm) {
MultipartFile mFile = importForm.getFile();
List<String> sheetNames = null;
List<String> columnNames = null;
try {
ExcelPorter excelPorter = new ExcelPorter(mFile.getInputStream(), mFile.getOriginalFilename());
if (excelPorter.getSheetNames().size() == 0) {
Alerts.setErrorMessage(model, "Unable to find movies table on any of the workbook sheets. Make sure your movies table has a header row and that the header for the movie title has the word \"title\" in it.");
return importCollections(model, principal);
}
sheetNames = excelPorter.getSheetNames();
columnNames = excelPorter.getAllColumnNames();
model.addAttribute("sheetOptions", ViewUtil.getOptions(sheetNames));
model.addAttribute("columnOptions", ViewUtil.getOptions(columnNames));
model.addAttribute("importOptionsForm", new ImportOptionsForm(excelPorter.getSheetNames(), excelPorter.getAllColumnNames()));
} catch (IOException ioe) {
Alerts.setErrorMessage(model, "File could not be successfully uploaded.");
return importCollections(model, principal);
}
ViewUtil.setImportedCollectionFile(session, mFile, sheetNames, columnNames);
return "collection/importOptions";
}
/**
* Import a movie collection. This performs the final import of the movie collection using the user options provided.
*
* @param model model
* @param principal principal
* @param session session
* @param importOptionsForm form of users import options
* @param result binding result
*
* @return collections management page
*/
@RequestMapping("/collections/importOptionsSubmit")
public String importOptionsSubmit(Model model, Principal principal, HttpSession session,
@ModelAttribute("importOptionsForm") @Valid ImportOptionsForm importOptionsForm,
BindingResult result) {
if (result.hasErrors()) {
List<String> sheets = ViewUtil.getImportedCollectionSheets(session);
List<String> columns = ViewUtil.getImportedCollectionColumns(session);
model.addAttribute("sheetOptions", ViewUtil.getOptions(sheets));
model.addAttribute("columnOptions", ViewUtil.getOptions(columns));
return "collection/importOptions";
}
MultipartFile mFile = ViewUtil.getImportedCollectionFile(session);
try {
collectionService.importCollection(mFile, importOptionsForm.getCollectionName(), importOptionsForm.isCloud(),
importOptionsForm.getSheetNames(), importOptionsForm.getColumnNames(), principal.getName());
Alerts.setMessage(model, "Collection imported.");
} catch (Exception e) {
LOGGER.error("Unable to import collection from Excel.", e);
Alerts.setErrorMessage(model, "Your collection could not be imported.");
}
ViewUtil.clearImportedCollectionFile(session);
return collections(model, principal);
}
}