package uk.ac.ox.zoo.seeg.abraid.mp.publicsite.web.admin;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
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.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.*;
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.ModelRunService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.workflow.ModelRunWorkflowService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.workflow.support.BatchDatesValidator;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.workflow.support.ModelRunWorkflowException;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.workflow.support.SourceCodeManager;
import uk.ac.ox.zoo.seeg.abraid.mp.common.web.AbstractController;
import uk.ac.ox.zoo.seeg.abraid.mp.publicsite.domain.*;
import java.io.IOException;
import java.util.*;
import static ch.lambdaj.Lambda.on;
import static ch.lambdaj.Lambda.sort;
import static org.springframework.util.StringUtils.hasText;
/**
* Controller for the disease group page of system administration.
* Copyright (c) 2014 University of Oxford
*/
@Controller
public class AdminDiseaseGroupController extends AbstractController {
private static final Logger LOGGER = Logger.getLogger(AdminDiseaseGroupController.class);
private static final String DISEASE_GROUP_JSON_CONVERSION_ERROR = "Cannot convert disease groups to JSON";
private static final String MODEL_MODES_WARNING = "Cannot get model modes";
private static final String SAVE_DISEASE_GROUP_SUCCESS = "Successfully saved changes to disease group %d (%s)";
private static final String SAVE_DISEASE_GROUP_ERROR = "Error saving changes to disease group %d (%s)";
private static final String ADD_DISEASE_GROUP_SUCCESS = "Successfully added new disease group %d (%s)";
private static final String ADD_DISEASE_GROUP_ERROR = "Error adding new disease group (%s)";
private static final String INVALID_COMBINATION_OF_INPUTS_ERROR = "Invalid combination of inputs";
/** The base URL for the system administration disease group controller methods. */
public static final String ADMIN_DISEASE_GROUP_BASE_URL = "/admin/diseases";
private DiseaseService diseaseService;
private AbraidJsonObjectMapper objectMapper;
private ModelRunWorkflowService modelRunWorkflowService;
private ModelRunService modelRunService;
private DiseaseOccurrenceSpreadHelper helper;
private BatchDatesValidator batchDatesValidator;
private SourceCodeManager sourceCodeManager;
private Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
};
@Autowired
public AdminDiseaseGroupController(DiseaseService diseaseService, AbraidJsonObjectMapper objectMapper,
ModelRunWorkflowService modelRunWorkflowService,
ModelRunService modelRunService, DiseaseOccurrenceSpreadHelper helper,
BatchDatesValidator batchDatesValidator,
SourceCodeManager sourceCodeManager) {
this.diseaseService = diseaseService;
this.objectMapper = objectMapper;
this.modelRunWorkflowService = modelRunWorkflowService;
this.modelRunService = modelRunService;
this.helper = helper;
this.batchDatesValidator = batchDatesValidator;
this.sourceCodeManager = sourceCodeManager;
}
/**
* Returns the initial view to display.
* @param model The model.
* @return The ftl page name.
* @throws com.fasterxml.jackson.core.JsonProcessingException if there is an error during JSON serialization
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(value = ADMIN_DISEASE_GROUP_BASE_URL + "/", method = RequestMethod.GET)
public String showPage(Model model) throws JsonProcessingException {
try {
List<DiseaseGroup> diseaseGroups = getSortedDiseaseGroups();
String diseaseGroupsJson = convertDiseaseGroupsToJson(diseaseGroups);
model.addAttribute("diseaseGroups", diseaseGroupsJson);
List<ValidatorDiseaseGroup> validatorDiseaseGroups = getSortedValidatorDiseaseGroups();
String validatorDiseaseGroupsJson = convertValidatorDiseaseGroupsToJson(validatorDiseaseGroups);
model.addAttribute("validatorDiseaseGroups", validatorDiseaseGroupsJson);
Set<String> modes = new HashSet<>();
try {
modes.addAll(sourceCodeManager.getSupportedModesForCurrentVersion());
} catch (IOException e) {
LOGGER.warn(MODEL_MODES_WARNING);
}
model.addAttribute("supportedModes", convertModesToJson(modes));
return "admin/diseasegroups/index";
} catch (JsonProcessingException e) {
LOGGER.error(DISEASE_GROUP_JSON_CONVERSION_ERROR, e);
throw e;
}
}
/**
* Returns model run information for the specified disease group.
* @param diseaseGroupId The id of the disease group for which to return model run information.
* @return Model run information in a JSON object.
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(
value = ADMIN_DISEASE_GROUP_BASE_URL + "/{diseaseGroupId}/modelruninformation",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<JsonModelRunInformation> getModelRunInformation(@PathVariable Integer diseaseGroupId) {
ModelRun lastRequestedModelRun = modelRunService.getLastRequestedModelRun(diseaseGroupId);
ModelRun lastCompletedModelRun = modelRunService.getMostRecentlyFinishedModelRunWhichCompleted(diseaseGroupId);
DiseaseOccurrenceStatistics statistics = diseaseService.getDiseaseOccurrenceStatistics(diseaseGroupId);
DiseaseGroup diseaseGroup = diseaseService.getDiseaseGroupById(diseaseGroupId);
List<DiseaseOccurrence> goldStandardOccurrences = diseaseService.getDiseaseOccurrencesForModelRunRequest(
diseaseGroupId, true);
boolean useBias = diseaseService.modelModeRequiresBiasDataForDisease(diseaseGroup);
long bespokeBiasCount = 0;
long usableBiasEstimate = 0;
if (useBias) {
bespokeBiasCount = diseaseService.getCountOfUnfilteredBespokeBiasOccurrences(diseaseGroup);
usableBiasEstimate = bespokeBiasCount == 0 ?
diseaseService.getEstimateCountOfFilteredDefaultBiasOccurrences(diseaseGroup) :
diseaseService.getEstimateCountOfFilteredBespokeBiasOccurrences(diseaseGroup);
}
JsonModelRunInformation info = new JsonModelRunInformationBuilder()
.populateLastModelRunText(lastRequestedModelRun)
.populateHasModelBeenSuccessfullyRun(lastCompletedModelRun)
.populateDiseaseOccurrencesText(statistics)
.populateCanRunModelWithReason(diseaseGroup)
.populateBatchDateParameters(lastCompletedModelRun, statistics)
.populateHasGoldStandardOccurrences(goldStandardOccurrences)
.populateBiasMessage(useBias, bespokeBiasCount, usableBiasEstimate)
.get();
return new ResponseEntity<>(info, HttpStatus.OK);
}
/**
* Generates a disease extent for the specified disease group.
* @param diseaseGroupId The id of the disease group.
* @param onlyUseGoldStandardOccurrences True if only "gold standard" occurrences should be used, otherwise false.
* @return An error status: 204 for success, 404 if disease group cannot be found in database.
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(value = ADMIN_DISEASE_GROUP_BASE_URL + "/{diseaseGroupId}/generatediseaseextent",
method = RequestMethod.POST)
@ResponseBody
public ResponseEntity generateDiseaseExtent(@PathVariable int diseaseGroupId,
boolean onlyUseGoldStandardOccurrences) {
DiseaseGroup diseaseGroup = diseaseService.getDiseaseGroupById(diseaseGroupId);
if (diseaseGroup == null) {
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
DiseaseProcessType processType = onlyUseGoldStandardOccurrences ?
DiseaseProcessType.MANUAL_GOLD_STANDARD : DiseaseProcessType.MANUAL;
modelRunWorkflowService.generateDiseaseExtent(diseaseGroupId, processType);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
/**
* Requests a model run for the specified disease group.
* @param diseaseGroupId The id of the disease group for which to request the model run.
* @param batchStartDate The start date of the occurrences batch. Must be in ISO 8601 format for correct parsing.
* @param batchEndDate The end date of the occurrences batch. Must be in ISO 8601 format for correct parsing.
* @param onlyUseGoldStandardOccurrences True if only "gold standard" occurrences should be used, otherwise false.
* @return An error message string (empty if no error).
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(
value = ADMIN_DISEASE_GROUP_BASE_URL + "/{diseaseGroupId}/requestmodelrun",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public synchronized ResponseEntity<String> requestModelRun(@PathVariable int diseaseGroupId, String batchStartDate,
String batchEndDate,
boolean onlyUseGoldStandardOccurrences) {
DiseaseGroup diseaseGroup = diseaseService.getDiseaseGroupById(diseaseGroupId);
if (diseaseGroup == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
if (!checkCorrectModelRunParametersProvided(onlyUseGoldStandardOccurrences, batchStartDate, batchEndDate)) {
return new ResponseEntity<>(INVALID_COMBINATION_OF_INPUTS_ERROR, HttpStatus.BAD_REQUEST);
}
try {
if (onlyUseGoldStandardOccurrences) {
modelRunWorkflowService.prepareForAndRequestModelRun(
diseaseGroupId, DiseaseProcessType.MANUAL_GOLD_STANDARD, null, null);
} else {
// Ensure that the batch date range is from start of day to end of day, then validate the dates
DateTime parsedBatchStartDate = parseAndShiftToStartOfDay(batchStartDate);
DateTime parsedBatchEndDate = parseAndShiftToEndOfDay(batchEndDate);
batchDatesValidator.validate(diseaseGroup, parsedBatchStartDate, parsedBatchEndDate);
modelRunWorkflowService.prepareForAndRequestModelRun(
diseaseGroupId, DiseaseProcessType.MANUAL, parsedBatchStartDate, parsedBatchEndDate);
}
return new ResponseEntity<>(HttpStatus.OK);
} catch (ModelRunWorkflowException | IllegalArgumentException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}
/**
* Enables automatic model runs for the specified disease group.
* @param diseaseGroupId The id of the disease group for which to enable automatic model runs.
* @return An error status: 204 for success, 404 if disease group cannot be found in database.
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(value = ADMIN_DISEASE_GROUP_BASE_URL + "/{diseaseGroupId}/automaticmodelruns",
method = RequestMethod.POST)
@ResponseBody
public ResponseEntity enableAutomaticModelRuns(@PathVariable int diseaseGroupId) {
DiseaseGroup diseaseGroup = diseaseService.getDiseaseGroupById(diseaseGroupId);
if (diseaseGroup != null) {
modelRunWorkflowService.enableAutomaticModelRuns(diseaseGroupId);
return new ResponseEntity(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
}
/**
* Save the updated values of the disease group's parameters.
* @param diseaseGroupId The id of the disease group.
* @param settings The JSON containing the new values to save.
* @return HTTP Status code: 204 for success, 400 if any inputs are invalid.
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(value = ADMIN_DISEASE_GROUP_BASE_URL + "/{diseaseGroupId}/save",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE)
@Transactional(rollbackFor = Exception.class)
public ResponseEntity save(@PathVariable int diseaseGroupId, @RequestBody JsonDiseaseGroup settings) {
DiseaseGroup diseaseGroup = diseaseService.getDiseaseGroupById(diseaseGroupId);
if ((diseaseGroup != null) && validInputs(settings)) {
if (saveProperties(diseaseGroup, settings)) {
LOGGER.info(String.format(SAVE_DISEASE_GROUP_SUCCESS, diseaseGroupId, settings.getName()));
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
LOGGER.info(String.format(SAVE_DISEASE_GROUP_ERROR, diseaseGroupId, settings.getName()));
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
/**
* Add a new disease group, with the provided parameters.
* @param settings The new values to be saved.
* @return HTTP Status code: 204 for success, 400 if any inputs are invalid.
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(value = ADMIN_DISEASE_GROUP_BASE_URL + "/add",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE)
@Transactional(rollbackFor = Exception.class)
public ResponseEntity add(@RequestBody JsonDiseaseGroup settings) {
if (validInputs(settings)) {
DiseaseGroup diseaseGroup = new DiseaseGroup();
if (saveProperties(diseaseGroup, settings)) {
LOGGER.info(String.format(ADD_DISEASE_GROUP_SUCCESS, diseaseGroup.getId(), settings.getName()));
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
LOGGER.info(String.format(ADD_DISEASE_GROUP_ERROR, settings.getName()));
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
/**
* Gets a page containing the disease occurrence spread table, for the specified disease group.
* @param model The model.
* @param diseaseGroupId The id of the disease group.
* @return The disease occurrence spread table page.
*/
@Secured({ "ROLE_ADMIN" })
@RequestMapping(value = ADMIN_DISEASE_GROUP_BASE_URL + "/{diseaseGroupId}/spread", method = RequestMethod.GET)
public String getDiseaseOccurrenceSpread(Model model, @PathVariable int diseaseGroupId) {
DiseaseOccurrenceSpreadTable table = helper.getDiseaseOccurrenceSpreadTable(diseaseGroupId);
model.addAttribute("table", table);
return "admin/diseasegroups/occurrencespread";
}
private List<DiseaseGroup> getSortedDiseaseGroups() {
List<DiseaseGroup> diseaseGroups = diseaseService.getAllDiseaseGroups();
return sort(diseaseGroups, on(DiseaseGroup.class).getName(), caseInsensitiveComparator);
}
private String convertDiseaseGroupsToJson(List<DiseaseGroup> diseaseGroups) throws JsonProcessingException {
List<JsonDiseaseGroup> jsonDiseaseGroups = new ArrayList<>();
for (DiseaseGroup diseaseGroup : diseaseGroups) {
jsonDiseaseGroups.add(new JsonDiseaseGroup(diseaseGroup));
}
return objectMapper.writeValueAsString(jsonDiseaseGroups);
}
private List<ValidatorDiseaseGroup> getSortedValidatorDiseaseGroups() {
List<ValidatorDiseaseGroup> validatorDiseaseGroups = diseaseService.getAllValidatorDiseaseGroups();
return sort(validatorDiseaseGroups, on(ValidatorDiseaseGroup.class).getName(), caseInsensitiveComparator);
}
private String convertValidatorDiseaseGroupsToJson(List<ValidatorDiseaseGroup> validatorDiseaseGroups)
throws JsonProcessingException {
List<JsonValidatorDiseaseGroup> jsonValidatorDiseaseGroups = new ArrayList<>();
for (ValidatorDiseaseGroup validatorDiseaseGroup : validatorDiseaseGroups) {
jsonValidatorDiseaseGroups.add(new JsonValidatorDiseaseGroup(validatorDiseaseGroup));
}
return objectMapper.writeValueAsString(jsonValidatorDiseaseGroups);
}
private String convertModesToJson(Set<String> modes)
throws JsonProcessingException {
return objectMapper.writeValueAsString(modes);
}
private boolean validInputs(JsonDiseaseGroup settings) {
String groupType = settings.getGroupType();
return hasText(settings.getName()) && hasText(groupType) && isValidGroupType(groupType);
}
private boolean isValidGroupType(String groupType) {
try {
DiseaseGroupType.valueOf(groupType);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
private boolean saveProperties(DiseaseGroup diseaseGroup, JsonDiseaseGroup settings) {
diseaseGroup.setName(settings.getName());
diseaseGroup.setPublicName(settings.getPublicName());
diseaseGroup.setShortName(settings.getShortName());
diseaseGroup.setAbbreviation(settings.getAbbreviation());
diseaseGroup.setGroupType(DiseaseGroupType.valueOf(settings.getGroupType()));
diseaseGroup.setAgentType(parseAgentType(settings.getAgentType()));
diseaseGroup.setGlobal(settings.getIsGlobal());
diseaseGroup.setFilterBiasDataByAgentType(settings.getFilterBiasDataByAgentType());
diseaseGroup.setModelMode(settings.getModelMode());
diseaseGroup.setMaxDaysBetweenModelRuns(settings.getMaxDaysBetweenModelRuns());
diseaseGroup.setMinNewLocationsTrigger(settings.getMinNewLocations());
diseaseGroup.setMaxEnvironmentalSuitabilityForTriggering(
settings.getMaxEnvironmentalSuitabilityForTriggering());
diseaseGroup.setMinDistanceFromDiseaseExtentForTriggering(
settings.getMinDistanceFromDiseaseExtentForTriggering());
diseaseGroup.setMinDataVolume(settings.getMinDataVolume());
diseaseGroup.setMinDistinctCountries(settings.getMinDistinctCountries());
diseaseGroup.setMinHighFrequencyCountries(settings.getMinHighFrequencyCountries());
diseaseGroup.setHighFrequencyThreshold(settings.getHighFrequencyThreshold());
diseaseGroup.setOccursInAfrica(settings.getOccursInAfrica());
diseaseGroup.setUseMachineLearning(settings.getUseMachineLearning());
diseaseGroup.setMaxEnvironmentalSuitabilityWithoutML(settings.getMaxEnvironmentalSuitabilityWithoutML());
setDiseaseExtentParameters(diseaseGroup, settings.getDiseaseExtentParameters());
if (setParentDiseaseGroup(diseaseGroup, settings) && setValidatorDiseaseGroup(diseaseGroup, settings)) {
diseaseService.saveDiseaseGroup(diseaseGroup);
return true;
} else {
return false;
}
}
private DiseaseGroupAgentType parseAgentType(String agentType) {
return StringUtils.isEmpty(agentType) ? null : DiseaseGroupAgentType.valueOf(agentType);
}
private boolean setParentDiseaseGroup(DiseaseGroup diseaseGroup, JsonDiseaseGroup settings) {
if (!hasParentDiseaseGroupSpecified(settings) || diseaseGroup.getGroupType() == DiseaseGroupType.CLUSTER) {
return true;
}
Integer parentId = settings.getParentDiseaseGroup().getId();
DiseaseGroup parentDiseaseGroup = diseaseService.getDiseaseGroupById(parentId);
diseaseGroup.setParentGroup(parentDiseaseGroup);
return (parentDiseaseGroup != null);
}
private boolean hasParentDiseaseGroupSpecified(JsonDiseaseGroup settings) {
return ((settings.getParentDiseaseGroup() != null) && (settings.getParentDiseaseGroup().getId() != null));
}
private boolean setValidatorDiseaseGroup(DiseaseGroup diseaseGroup, JsonDiseaseGroup settings) {
if (!hasValidatorDiseaseGroupSpecified(settings)) {
return true;
}
Integer validatorId = settings.getValidatorDiseaseGroup().getId();
ValidatorDiseaseGroup validatorDiseaseGroup = diseaseService.getValidatorDiseaseGroupById(validatorId);
diseaseGroup.setValidatorDiseaseGroup(validatorDiseaseGroup);
return (validatorDiseaseGroup != null);
}
private boolean hasValidatorDiseaseGroupSpecified(JsonDiseaseGroup settings) {
return ((settings.getValidatorDiseaseGroup() != null) && (settings.getValidatorDiseaseGroup().getId() != null));
}
private void setDiseaseExtentParameters(DiseaseGroup diseaseGroup, JsonDiseaseExtent newValues) {
if (newValues != null) {
if (diseaseGroup.getDiseaseExtentParameters() == null) {
addDiseaseExtent(diseaseGroup, newValues);
} else {
updateDiseaseExtent(diseaseGroup, newValues);
}
}
}
private void addDiseaseExtent(DiseaseGroup diseaseGroup, JsonDiseaseExtent newValues) {
DiseaseExtent parameters = new DiseaseExtent(
diseaseGroup,
newValues.getMinValidationWeighting(),
newValues.getMaxMonthsAgoForHigherOccurrenceScore(),
newValues.getLowerOccurrenceScore(),
newValues.getHigherOccurrenceScore()
);
diseaseGroup.setDiseaseExtentParameters(parameters);
}
private void updateDiseaseExtent(DiseaseGroup diseaseGroup, JsonDiseaseExtent newValues) {
DiseaseExtent parameters = diseaseGroup.getDiseaseExtentParameters();
parameters.setMinValidationWeighting(newValues.getMinValidationWeighting());
parameters.setMaxMonthsAgoForHigherOccurrenceScore(newValues.getMaxMonthsAgoForHigherOccurrenceScore());
parameters.setLowerOccurrenceScore(newValues.getLowerOccurrenceScore());
parameters.setHigherOccurrenceScore(newValues.getHigherOccurrenceScore());
}
private boolean checkCorrectModelRunParametersProvided(
boolean onlyUseGoldStandardOccurrences, String batchStartDate, String batchEndDate) {
boolean goldRun = (onlyUseGoldStandardOccurrences) && (batchEndDate == null) && (batchStartDate == null);
boolean manualRun = (!onlyUseGoldStandardOccurrences) && (batchEndDate != null) && (batchStartDate != null);
return goldRun || manualRun;
}
private DateTime parseAndShiftToStartOfDay(String dateString) throws IllegalArgumentException {
return parseDateTime(dateString).withTimeAtStartOfDay();
}
private DateTime parseAndShiftToEndOfDay(String dateString) throws IllegalArgumentException {
return parseAndShiftToStartOfDay(dateString).plusDays(1).minusMillis(1);
}
private DateTime parseDateTime(String dateString) throws IllegalArgumentException {
DateTime parsed = DateTime.parse(dateString);
if (parsed == null) {
throw new IllegalArgumentException("Could not parse date time");
}
return parsed;
}
}