package uk.ac.ox.zoo.seeg.abraid.mp.publicsite.web.user.registration;
import ch.lambdaj.function.convert.Converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.Expert;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.ValidatorDiseaseGroup;
import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.json.AbraidJsonObjectMapper;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.DiseaseService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.EmailService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.ExpertService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.web.AbstractController;
import uk.ac.ox.zoo.seeg.abraid.mp.publicsite.domain.JsonExpertBasic;
import uk.ac.ox.zoo.seeg.abraid.mp.publicsite.domain.JsonExpertDetails;
import uk.ac.ox.zoo.seeg.abraid.mp.publicsite.domain.JsonValidatorDiseaseGroup;
import uk.ac.ox.zoo.seeg.abraid.mp.publicsite.security.CurrentUserService;
import javax.servlet.ServletRequest;
import java.util.*;
import static ch.lambdaj.Lambda.*;
import static org.hamcrest.collection.IsIn.isIn;
/**
* Controller for the expert registration process.
* Copyright (c) 2014 University of Oxford
*/
@Controller
@SessionAttributes(RegistrationController.EXPERT_SESSION_STATE_KEY)
public class RegistrationController extends AbstractController {
/** Session key for Expert object. */
public static final String EXPERT_SESSION_STATE_KEY = "expert";
private static final Logger LOGGER = Logger.getLogger(RegistrationController.class);
private static final String LOG_NEW_USER_CREATED = "New user created: %s";
private static final String ALERTS_ATTRIBUTE_KEY = "alerts";
private static final String CAPTCHA_ATTRIBUTE_KEY = "captcha";
private static final String DISEASES_ATTRIBUTE_KEY = "diseases";
private static final String JSON_EXPERT_ATTRIBUTE_KEY = "jsonExpert";
private static final String ERROR_LOGGED_IN_USERS_CANNOT_CREATE_NEW_ACCOUNTS =
"Logged in users cannot create new accounts";
private static final String ERROR_INVALID_REGISTRATION_SESSION =
"Invalid registration session";
private static final String EMAIL_DATA_KEY = "expert";
private static final String EMAIL_TEMPLATE = "registration/newUserEmail.ftl";
private static final String EMAIL_SUBJECT = "New user requiring visibility sign off";
private final CurrentUserService currentUserService;
private final ExpertService expertService;
private final DiseaseService diseaseService;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
private final AbraidJsonObjectMapper json;
private final RegistrationControllerValidator validator;
private final UserDetailsService userDetailsService;
@Autowired
public RegistrationController(CurrentUserService currentUserService, ExpertService expertService,
DiseaseService diseaseService, EmailService emailService,
PasswordEncoder passwordEncoder, AbraidJsonObjectMapper objectMapper,
RegistrationControllerValidator expertRegistrationValidator,
UserDetailsService userDetailsService) {
this.currentUserService = currentUserService;
this.expertService = expertService;
this.diseaseService = diseaseService;
this.emailService = emailService;
this.passwordEncoder = passwordEncoder;
this.json = objectMapper;
this.validator = expertRegistrationValidator;
this.userDetailsService = userDetailsService;
}
private static void updateExpert(Expert expert, JsonExpertBasic expertBasic) {
expert.setEmail(expertBasic.getEmail());
expert.setPassword(expertBasic.getPassword());
}
/**
* Starts a registration session and loads the first account registration page.
* @param modelMap The templating/session model.
* @param status The session status holder.
* @return The template to render.
* @throws JsonProcessingException Thrown if issue generating json for bootstrapped variables.
*/
@RequestMapping(value = "/register/account", method = RequestMethod.GET)
public String getAccountPage(ModelMap modelMap, SessionStatus status) throws JsonProcessingException {
if (checkIfUserLoggedIn()) {
// Logged in user, stop registration session, redirect to home page
status.setComplete();
return "redirect:/";
}
List<String> validationFailures = new ArrayList<>();
Expert expert;
if (!modelMap.containsAttribute(EXPERT_SESSION_STATE_KEY)) {
// Create an empty expert in the session state
expert = new Expert();
modelMap.addAttribute(EXPERT_SESSION_STATE_KEY, expert);
} else {
expert = (Expert) modelMap.get(EXPERT_SESSION_STATE_KEY);
if (expert.getEmail() != null || expert.getPassword() != null) {
validationFailures = validator.validateBasicFields(expert);
}
}
modelMap.addAttribute(ALERTS_ATTRIBUTE_KEY, json.writeValueAsString(validationFailures));
modelMap.addAttribute(CAPTCHA_ATTRIBUTE_KEY, validator.createValidationCaptcha());
modelMap.addAttribute(JSON_EXPERT_ATTRIBUTE_KEY, json.writeValueAsString(new JsonExpertBasic(expert)));
// Return registration form
return "register/account";
}
/**
* Receives the user input from the first account registration page and responds accordingly.
* @param modelMap The templating/session model.
* @param status The session status holder.
* @param expertBasic The user input from the first account registration page.
* @param request The http request object, used for captcha validation.
* @return A failure status with an array of response messages or a success status.
*/
@RequestMapping(value = "/register/account", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<String>> submitAccountPage(
ModelMap modelMap, SessionStatus status, @RequestBody JsonExpertBasic expertBasic, ServletRequest request) {
if (checkIfUserLoggedIn()) {
status.setComplete();
return new ResponseEntity<>(
Arrays.asList(ERROR_LOGGED_IN_USERS_CANNOT_CREATE_NEW_ACCOUNTS), HttpStatus.FORBIDDEN);
}
if (!modelMap.containsAttribute(EXPERT_SESSION_STATE_KEY)) {
status.setComplete();
return new ResponseEntity<>(
Arrays.asList(ERROR_INVALID_REGISTRATION_SESSION), HttpStatus.FORBIDDEN);
}
Expert expert = (Expert) modelMap.get(EXPERT_SESSION_STATE_KEY);
// Update expert
updateExpert(expert, expertBasic);
// Validate
List<String> validationFailures = validator.validateBasicFields(expert);
validationFailures.addAll(validator.validateTransientFields(expertBasic, request));
if (validationFailures.size() > 0) {
expert.setEmail(null);
expert.setPassword(null);
return new ResponseEntity<>(validationFailures, HttpStatus.BAD_REQUEST);
}
// Return registration form
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
/**
* Loads the second account registration page, after checking for a valid registration session.
* @param modelMap The templating/session model.
* @param status The session status holder.
* @return The template to render.
* @throws JsonProcessingException Thrown if issue generating json for bootstrapped variables.
*/
@RequestMapping(value = "/register/details", method = RequestMethod.GET)
public String getDetailsPage(ModelMap modelMap, SessionStatus status) throws JsonProcessingException {
if (checkIfUserLoggedIn()) {
// Logged in user, stop registration session, redirect to home page
status.setComplete();
return "redirect:/";
}
if (!modelMap.containsAttribute(EXPERT_SESSION_STATE_KEY)) {
// Make sure page one has already been filled in
return "redirect:/register/account";
}
Expert expert = (Expert) modelMap.get(EXPERT_SESSION_STATE_KEY);
if (!validator.validateBasicFields(expert).isEmpty()) {
// Make sure that the data on the first first page was valid
return "redirect:/register/account";
}
List<JsonValidatorDiseaseGroup> allValidatorDiseaseGroups = loadValidatorDiseaseGroups();
modelMap.addAttribute(DISEASES_ATTRIBUTE_KEY, json.writeValueAsString(allValidatorDiseaseGroups));
modelMap.addAttribute(JSON_EXPERT_ATTRIBUTE_KEY, json.writeValueAsString(new JsonExpertDetails(expert)));
return "register/details";
}
/**
* Receives the user input from the second account registration page and responds accordingly.
* @param modelMap The templating/session model.
* @param status The session status holder.
* @param expertDetails The user input from the second account registration page.
* @return A failure status with an array of response messages or a success status.
*/
@RequestMapping(value = "/register/details", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<String>> submitDetailsPage(
ModelMap modelMap, SessionStatus status, @RequestBody JsonExpertDetails expertDetails) {
if (checkIfUserLoggedIn()) {
status.setComplete();
return new ResponseEntity<>(
Arrays.asList(ERROR_LOGGED_IN_USERS_CANNOT_CREATE_NEW_ACCOUNTS), HttpStatus.FORBIDDEN);
}
if (!modelMap.containsAttribute(EXPERT_SESSION_STATE_KEY)) {
status.setComplete();
return new ResponseEntity<>(
Arrays.asList(ERROR_INVALID_REGISTRATION_SESSION), HttpStatus.FORBIDDEN);
}
Expert expert = (Expert) modelMap.get(EXPERT_SESSION_STATE_KEY);
// Update expert
updateExpert(expert, expertDetails);
// Validate
List<String> validationFailures = validator.validateDetailsFields(expert);
if (!validationFailures.isEmpty()) {
return new ResponseEntity<>(validationFailures, HttpStatus.BAD_REQUEST);
}
validationFailures = validator.validateBasicFields(expert);
if (!validationFailures.isEmpty()) {
// Email must have been snipped, so send back to page 1
return new ResponseEntity<>(validationFailures, HttpStatus.CONFLICT);
}
// Hash password
expert.setPassword(passwordEncoder.encode(expert.getPassword()));
// Save to db
expertService.saveExpert(expert);
LOGGER.info(String.format(LOG_NEW_USER_CREATED, expert.getEmail()));
emailAdmin(expert);
// Return successfully
status.setComplete();
automaticLogin(expert);
return new ResponseEntity<>(HttpStatus.CREATED); // Could add success page
}
private void automaticLogin(Expert expert) {
UserDetails userDetails = userDetailsService.loadUserByUsername(expert.getEmail());
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(token);
}
private void emailAdmin(Expert expert) {
if (expert.getVisibilityRequested()) {
Map<String, Object> data = new HashMap<>();
data.put(EMAIL_DATA_KEY, expert);
emailService.sendEmailInBackground(EMAIL_SUBJECT, EMAIL_TEMPLATE, data);
}
}
private List<JsonValidatorDiseaseGroup> loadValidatorDiseaseGroups() {
List<ValidatorDiseaseGroup> allValidatorDiseaseGroups = diseaseService.getAllValidatorDiseaseGroups();
return convert(allValidatorDiseaseGroups, new Converter<ValidatorDiseaseGroup, JsonValidatorDiseaseGroup>() {
@Override
public JsonValidatorDiseaseGroup convert(ValidatorDiseaseGroup validatorDiseaseGroup) {
return new JsonValidatorDiseaseGroup(validatorDiseaseGroup);
}
});
}
private boolean checkIfUserLoggedIn() {
return currentUserService.getCurrentUserId() != null;
}
private void updateExpert(Expert expert, JsonExpertDetails expertDetails) {
expert.setName(expertDetails.getName());
expert.setVisibilityRequested(expertDetails.getVisibilityRequested());
expert.setJobTitle(expertDetails.getJobTitle());
expert.setInstitution(expertDetails.getInstitution());
List<ValidatorDiseaseGroup> allValidatorDiseaseGroups = diseaseService.getAllValidatorDiseaseGroups();
List<ValidatorDiseaseGroup> interests = filter(
having(on(ValidatorDiseaseGroup.class).getId(), isIn(expertDetails.getDiseaseInterests())),
allValidatorDiseaseGroups);
expert.setValidatorDiseaseGroups(interests);
}
}