HomeController.java
package org.xandercat.pmdb.controller;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
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.GetMapping;
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.bind.annotation.ResponseBody;
import org.xandercat.pmdb.dto.Movie;
import org.xandercat.pmdb.dto.MovieCollection;
import org.xandercat.pmdb.dto.MovieStatistics;
import org.xandercat.pmdb.exception.WebServicesException;
import org.xandercat.pmdb.exception.CollectionSharingException;
import org.xandercat.pmdb.form.movie.MovieForm;
import org.xandercat.pmdb.form.movie.SearchForm;
import org.xandercat.pmdb.service.CollectionService;
import org.xandercat.pmdb.service.ImdbAttribute;
import org.xandercat.pmdb.service.MovieService;
import org.xandercat.pmdb.util.Alerts;
import org.xandercat.pmdb.util.DoubleStatistics;
import org.xandercat.pmdb.util.LongStatistics;
import org.xandercat.pmdb.util.ViewUtil;
import org.xandercat.pmdb.util.WordStatistics;
import org.xandercat.pmdb.util.format.FormatUtil;
/**
* Controller for movie functions on the user's active movie collection. Primarily includes
* searching, adding, editing, and deleting movies.
*
* @author Scott Arnold
*/
@Controller
public class HomeController {
private static final Logger LOGGER = LogManager.getLogger(HomeController.class);
@Autowired
private CollectionService collectionService;
@Autowired
private MovieService movieService;
@ModelAttribute("viewTab")
public String getViewTab() {
return ViewUtil.TAB_HOME;
}
@ModelAttribute("attributeNames")
public List<String> getAttributeNames(Principal principal) {
Optional<MovieCollection> defaultMovieCollection = collectionService.getDefaultMovieCollection(principal.getName());
if (defaultMovieCollection.isPresent()) {
try {
return movieService.getAttributeKeysForCollection(defaultMovieCollection.get().getId(), principal.getName());
} catch (Exception e) {
LOGGER.error("Unable to add collection attribute names to model.", e);
}
}
return new ArrayList<String>();
}
/**
* Page for listing movies within user's default/active movie collection. This is considered the main "home" page
* of the application, where users can search and update their movie collection.
*
* @param model model
* @param principal principal
* @param session session
*
* @return home page
*/
@GetMapping("/")
public String home(Model model, Principal principal, HttpSession session) {
model.addAttribute("searchForm", new SearchForm());
return prepareHome(model, principal, null, session);
}
/**
* Search user's default/active movie collection. This filters the list of movies in the user's default/active movie
* collection to those that match their search.
*
* @param model model
* @param principal principal
* @param session session
* @param searchForm search form
* @param result binding result
*
* @return home page, filtered to only movies matching their search
*/
@RequestMapping("/movies/search")
public String search(Model model, Principal principal, HttpSession session,
@ModelAttribute("searchForm") @Valid SearchForm searchForm,
BindingResult result) {
return prepareHome(model, principal, searchForm.getSearchString(), session);
}
private String prepareHome(Model model, Principal principal, String searchString, HttpSession session) {
Optional<MovieCollection> defaultMovieCollection = collectionService.getDefaultMovieCollection(principal.getName());
if (!defaultMovieCollection.isPresent()) {
// send them to collections so they can set a default movie collection
return "redirect:/collections";
}
model.addAttribute("defaultMovieCollection", defaultMovieCollection.get());
try {
Set<Movie> movies = movieService.getMoviesForCollection(defaultMovieCollection.get().getId(), principal.getName());
model.addAttribute("totalMoviesInCollection", movies.size());
model.addAttribute("unlinkCount", movies.stream()
.filter(movie -> FormatUtil.isBlank(movie.getAttribute(ImdbAttribute.IMDB_ID.getKey())))
.count());
if (FormatUtil.isNotBlank(searchString)) {
movies = movieService.searchMoviesForCollection(defaultMovieCollection.get().getId(), searchString, principal.getName());
}
List<String> attrColumns = movieService.getTableColumnPreferences(principal.getName());
model.addAttribute("movies", movies);
model.addAttribute("attrFormatters", ViewUtil.getDataFormatters(movies, attrColumns));
model.addAttribute("attrColumns", attrColumns);
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to retrieve movies for default movie collection.", e);
Alerts.setErrorMessage(model, "Unable to get movies for the collection.");
}
return "movie/movies";
}
/**
* AJAX method for returning the full details of a single movie in the user's movie collection. Returned result is
* an HTML fragment that can be loaded into a dialog.
*
* @param model model
* @param principal principal
* @param movieId id of movie to get details for
*
* @return page fragment for movie details
*/
@RequestMapping("/movies/movieDetails")
public String movieDetails(Model model, Principal principal, @RequestParam String movieId) {
try {
Optional<MovieCollection> defaultMovieCollection = collectionService.getDefaultMovieCollection(principal.getName());
model.addAttribute("defaultMovieCollection", defaultMovieCollection.get());
Optional<Movie> movie = movieService.getMovie(movieId, principal.getName());
if (movie.isPresent()) {
model.addAttribute("movie", movie.get());
}
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to load movie id " + movieId + " for user " + principal.getName(), e);
}
return "movie/movieDetails";
}
@RequestMapping("/movies/movieStatistics")
public String movieStatistics(Model model, Principal principal, @RequestParam String movieId) {
try {
Optional<MovieCollection> defaultMovieCollection = collectionService.getDefaultMovieCollection(principal.getName());
model.addAttribute("defaultMovieCollection", defaultMovieCollection.get());
Set<Movie> movies = movieService.getMoviesForCollection(defaultMovieCollection.get().getId(), principal.getName());
Optional<Movie> movie = movieService.getMovie(movieId, principal.getName());
if (movie.isPresent()) {
model.addAttribute("movie", movie.get());
}
MovieStatistics movieStatistics = new MovieStatistics(movies);
Optional<DoubleStatistics> ratingStatistics = movieStatistics.getDoubleStatistics(ImdbAttribute.IMDB_RATING.getKey());
Optional<LongStatistics> voteStatistics = movieStatistics.getLenientLongStatistics(ImdbAttribute.IMDB_VOTES.getKey());
Optional<WordStatistics> genreStatistics = movieStatistics.getWordStatistics(ImdbAttribute.GENRE.getKey());
if (ratingStatistics.isPresent()) {
model.addAttribute("ratingStatistics", ratingStatistics.get());
}
if (voteStatistics.isPresent()) {
model.addAttribute("voteStatistics", voteStatistics.get());
}
if (genreStatistics.isPresent()) {
model.addAttribute("genreStatistics", genreStatistics.get());
model.addAttribute("genresMostCommon", genreStatistics.get().getTopWordCounts(3).stream()
.map(WordStatistics.WordCount::toString).collect(Collectors.joining(",")));
model.addAttribute("genresLeastCommon", genreStatistics.get().getBottomWordCounts(3).stream()
.map(WordStatistics.WordCount::toString).collect(Collectors.joining(",")));
if (movie.isPresent()) {
model.addAttribute("specificGenreCounts", genreStatistics.get().getWordCountsForWords(movie.get().getAttribute(ImdbAttribute.GENRE.getKey())));
}
}
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to load movie id " + movieId + " for user " + principal.getName(), e);
}
return "movie/movieStatistics";
}
/**
* Page to add a new movie to the movie collection (manual entry).
*
* @param model model
*
* @return page to add a new movie to the movie collection
*/
@RequestMapping("/movies/addMovie")
public String addMovie(Model model) {
model.addAttribute("movieForm", new MovieForm());
return "movie/addMovie";
}
/**
* Page to edit a movie in the movie collection.
*
* @param model model
* @param principal principal
* @param movieId id of movie to edit
* @param session session
*
* @return page to edit movie
*/
@RequestMapping("/movies/editMovie")
public String editMovie(Model model, Principal principal, @RequestParam String movieId, HttpSession session) {
try {
Optional<Movie> movie = movieService.getMovie(movieId, principal.getName());
if (!movie.isPresent()) {
throw new IllegalArgumentException("Movie ID " + movieId + " could not be retrieved.");
}
model.addAttribute("movieForm", new MovieForm(movie.get()));
} catch (Exception e) {
LOGGER.error("Unable to edit movie for ID: " + movieId, e);
Alerts.setErrorMessage(model, "This movie cannot be edited.");
return home(model, principal, session);
}
return "movie/editMovie";
}
/**
* Add a new movie to the movie collection (manual entry).
*
* @param model model
* @param principal principal
* @param session session
* @param movieForm movie form
* @param result binding result
*
* @return home page
*/
@RequestMapping("/movies/addMovieSubmit")
public String addMovieSubmit(Model model, Principal principal, HttpSession session,
@ModelAttribute("movieForm") @Valid MovieForm movieForm,
BindingResult result) {
if (result.hasErrors()) {
return "movie/addMovie";
}
Optional<MovieCollection> movieCollection = collectionService.getDefaultMovieCollection(principal.getName());
Movie movie = movieForm.toMovie();
movie.setCollectionId(movieCollection.get().getId());
try {
movieService.addMovie(movie, principal.getName());
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to add movie.", e);
Alerts.setErrorMessage(model, "This movie cannot be added.");
return home(model, principal, session);
}
Alerts.setMessage(model, "Movie added to the collection.");
return home(model, principal, session);
}
/**
* Update a movie in the movie collection.
*
* @param model model
* @param principal principal
* @param session session
* @param movieForm movie form
* @param result binding result
*
* @return home page
*/
@RequestMapping("/movies/editMovieSubmit")
public String editMovieSubmit(Model model, Principal principal, HttpSession session,
@ModelAttribute("movieForm") @Valid MovieForm movieForm,
BindingResult result) {
if (result.hasErrors()) {
return "movie/editMovie";
}
try {
Movie movieBefore = movieService.getMovie(movieForm.getId(), principal.getName()).get();
Movie movie = movieForm.toMovie();
movie.setId(movieBefore.getId());
movie.setCollectionId(movieBefore.getCollectionId());
movieService.updateMovie(movie, principal.getName());
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to update movie.", e);
Alerts.setErrorMessage(model, "This movie cannot be updated.");
return home(model, principal, session);
}
Alerts.setMessage(model, "Movie updated.");
return home(model, principal, session);
}
/**
* Delete movie from the movie collection. Confirmation should be handled on client side.
*
* @param model model
* @param principal principal
* @param movieId id of movie to delete
* @param session session
*
* @return home page
*/
@RequestMapping(value="/movies/deleteMovie", method=RequestMethod.POST)
public String deleteMovie(Model model, Principal principal, @RequestParam String movieId, HttpSession session) {
Optional<MovieCollection> movieCollection = collectionService.getDefaultMovieCollection(principal.getName());
try {
Movie movie = movieService.getMovie(movieId, principal.getName()).get();
if (!movie.getCollectionId().equals(movieCollection.get().getId())) {
Alerts.setErrorMessage(model, "Movies can only be deleted from your currently active collection.");
return home(model, principal, session);
}
movieService.deleteMovie(movieId, principal.getName());
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("Unable to delete movie.", e);
Alerts.setErrorMessage(model, "Movie cannot be deleted.");
return home(model, principal, session);
}
Alerts.setMessage(model, "Movie deleted.");
return home(model, principal, session);
}
/**
* Page for configuring what columns of data are included in the table of movies listed on the home page.
*
* @param model model
* @param principal principal
* @param session session
*
* @return page for configuring movie table columns
*/
@RequestMapping("/movies/configureColumns")
public String configureColumns(Model model, Principal principal, HttpSession session) {
Optional<MovieCollection> movieCollection = collectionService.getDefaultMovieCollection(principal.getName());
List<String> tableColumnOptions = null;
try {
tableColumnOptions = movieService.getAttributeKeysForCollection(movieCollection.get().getId(), principal.getName());
} catch (CollectionSharingException | WebServicesException e) {
LOGGER.error("User " + principal.getName() + " cannot configure columns for collection " + movieCollection.get().getId(), e);
Alerts.setErrorMessage(model, "Columns cannot be configured.");
return home(model, principal, session);
}
List<String> tableColumnPreferences = movieService.getTableColumnPreferences(principal.getName());
tableColumnOptions.removeAll(tableColumnPreferences); // remove ones already in preference list
model.addAttribute("tableColumnPreferences", tableColumnPreferences);
model.addAttribute("tableColumnOptions", tableColumnOptions);
return "movie/configureColumns";
}
/**
* Reorder columns shown on the home page table of movies using drag and drop indicies for columns.
*
* @param model model
* @param principal principal
* @param dragIndex index of column being moved
* @param dropIndex index of column where target column is being moved to
* @param session session
*
* @return page for configuring movie table columns
*/
@RequestMapping("/movies/reorderColumns")
public String reorderColumns(Model model, Principal principal, @RequestParam int dragIndex, @RequestParam int dropIndex, HttpSession session) {
LOGGER.debug("Requested drag from " + dragIndex + " to " + dropIndex);
try {
movieService.reorderTableColumnPreference(dragIndex, dropIndex, principal.getName());
// not going to set success messages here as it would reduce usability of the interface and be of little value
} catch (Exception e) {
LOGGER.error("Unable to reorder columns.", e);
Alerts.setErrorMessage(model, "Table columns could not be reordered.");
}
return configureColumns(model, principal, session);
}
/**
* Add a new attribute as a column in the table of movies shown on the home page.
*
* @param model model
* @param principal principal
* @param attributeName name of attribute to add as a column in the table
* @param session session
*
* @return page for configuring movie table columns
*/
@RequestMapping("/movies/addColumnPreference")
public String addColumnPreference(Model model, Principal principal, @RequestParam String attributeName, HttpSession session) {
movieService.addTableColumnPreference(attributeName, principal.getName());
// not going to set success messages here as it would reduce usability of the interface and be of little value
return configureColumns(model, principal, session);
}
/**
* Remove a column from the table of movies shown on the home page.
*
* @param model model
* @param principal principal
* @param deleteIndex index of column to be removed
* @param session session
*
* @return page for configuring movie table columns
*/
@RequestMapping("/movies/deleteColumnPreference")
public String deleteColumnPreference(Model model, Principal principal, @RequestParam int deleteIndex, HttpSession session) {
movieService.deleteTableColumnPreference(deleteIndex, principal.getName());
// not going to set success messages here as it would reduce usability of the interface and be of little value
return configureColumns(model, principal, session);
}
/**
* AJAX generic request to dismiss a "session alert". Once a session alert is dismissed, it will remain dismissed for the
* duration of the user's session. Provides no response.
*
* @param session session
* @param key key for alert
*/
@RequestMapping("/dismissSessionAlert")
public @ResponseBody void dismissSessionAlert(HttpSession session, @RequestParam String key) {
Alerts.dismissSessionAlert(session, key);
}
}