package uk.ac.ox.zoo.seeg.abraid.mp.publicsite.web.admin.covariates; import ch.lambdaj.collection.LambdaList; import ch.lambdaj.collection.LambdaMap; import ch.lambdaj.collection.LambdaSet; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.multipart.MultipartFile; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.CovariateFile; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.CovariateSubFile; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.DiseaseGroup; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.json.JsonCovariateConfiguration; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.json.JsonCovariateFile; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.json.JsonCovariateSubFile; import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.CovariateService; import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.DiseaseService; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static ch.lambdaj.Lambda.*; import static ch.lambdaj.collection.LambdaCollections.with; import static org.hamcrest.Matchers.isIn; /** * A validator for checking the user input for the actions associated with the CovariatesController. * Copyright (c) 2014 University of Oxford */ public class CovariatesControllerValidator { private static final String FAIL_FILE_MISSING = "File missing."; private static final String FAIL_NAME_MISSING = "Name missing."; private static final String FAIL_INVALID_PARENT = "Parent ID not valid."; private static final String FAIL_QUALIFIER_MISSING = "Qualifier missing."; private static final String FAIL_SUBDIRECTORY_MISSING = "Subdirectory missing."; private static final String FAIL_SUBDIRECTORY_NOT_VALID = "Subdirectory not valid."; private static final String FAIL_NAME_NOT_UNIQUE = "Name not unique."; private static final String FAIL_FILE_ALREADY_EXISTS = "File already exists."; private static final String FAIL_TARGET_PATH_NOT_VALID = "Target path not valid."; private static final String FAIL_FILES_IS_NULL = "'files' is null."; private static final String FAIL_FILES_PATH_DO_NOT_MATCH = "Unexpected file or subfile listed or missing."; private static final String FAIL_ENABLED_DISEASE_IDS_CONTAINS_DUPLICATES = "Enabled disease ids contains duplicates."; private static final String FAIL_UNKNOWN_DISEASE_ID_REFERENCED_BY_FILE = "One or more files specify usage for an unknown disease id."; private final CovariateService covariateService; private final DiseaseService diseaseService; @Autowired public CovariatesControllerValidator(CovariateService covariateService, DiseaseService diseaseService) { this.covariateService = covariateService; this.diseaseService = diseaseService; } /** * Validate the user input from a covariate upload. * @param name The display name for the covariate (null if a sub file). * @param qualifier The qualifier name for the covariate sub file (ie the year/month). * @param parentId The ID of the parent covariate for this file (or null if this is the first file). * @param subdirectory The directory to add the file to. * @param file The covariate file. * @param targetPath The location to store the file. * @return A set of validation failures. */ public Collection<String> validateCovariateUpload(String name, String qualifier, Integer parentId, String subdirectory, MultipartFile file, String targetPath) { List<String> messages = new ArrayList<>(); if (file == null || file.isEmpty()) { messages.add(FAIL_FILE_MISSING); } if (parentId != null && covariateService.getCovariateFileById(parentId) == null) { messages.add(FAIL_INVALID_PARENT); } if (parentId == null && StringUtils.isEmpty(name)) { messages.add(FAIL_NAME_MISSING); } if (StringUtils.isEmpty(qualifier)) { messages.add(FAIL_QUALIFIER_MISSING); } if (StringUtils.isEmpty(subdirectory)) { messages.add(FAIL_SUBDIRECTORY_MISSING); } if (!StringUtils.isEmpty(subdirectory) && checkForNonNormalPath(subdirectory)) { messages.add(FAIL_SUBDIRECTORY_NOT_VALID); } if (!checkCovariateNameUniqueness(name)) { messages.add(FAIL_NAME_NOT_UNIQUE); } if (messages.isEmpty()) { if (Paths.get(targetPath).toFile().exists()) { messages.add(FAIL_FILE_ALREADY_EXISTS); } if (!checkPathUnderCovariateDir(covariateService.getCovariateDirectory(), targetPath)) { messages.add(FAIL_TARGET_PATH_NOT_VALID); } } return messages; } private boolean checkForNonNormalPath(String subdirectory) { return subdirectory.contains("/./") || subdirectory.contains("/../") || subdirectory.contains("\\") || subdirectory.contains("//"); } private boolean checkCovariateNameUniqueness(String name) { return !extract(covariateService.getAllCovariateFiles(), on(CovariateFile.class).getName()).contains(name); } private boolean checkPathUnderCovariateDir(String covariateDirectory, String path) { Path parent = Paths.get(covariateDirectory).toAbsolutePath(); Path child = Paths.get(path).toAbsolutePath(); while (child != null && !child.equals(parent)) { child = child.getParent(); } return child != null; } /** * Validate the user input from a covariate configuration change. * @param config The new configuration. * @return A set of validation failures. */ public Collection<String> validateCovariateConfiguration(JsonCovariateConfiguration config) { List<String> messages = new ArrayList<>(); if (!checkFilesFieldForNull(config)) { messages.add(FAIL_FILES_IS_NULL); } else { if (!checkFileListAndSubFiles(config)) { messages.add(FAIL_FILES_PATH_DO_NOT_MATCH); } if (!checkEnabledDiseaseUniqueness(config)) { messages.add(FAIL_ENABLED_DISEASE_IDS_CONTAINS_DUPLICATES); } if (!checkDiseaseReferenceIntegrity(config)) { messages.add(FAIL_UNKNOWN_DISEASE_ID_REFERENCED_BY_FILE); } } return messages; } private boolean checkFilesFieldForNull(JsonCovariateConfiguration config) { return (config != null && config.getFiles() != null); } private boolean checkFileListAndSubFiles(JsonCovariateConfiguration config) { List<CovariateFile> knownCovariates = covariateService.getAllCovariateFiles(); List<JsonCovariateFile> configCovariates = config.getFiles(); LambdaMap<Integer, CovariateFile> knownCovariatesById = with(knownCovariates) .index(on(CovariateFile.class).getId()); LambdaMap<Integer, JsonCovariateFile> configCovariatesById = with(configCovariates) .index(on(JsonCovariateFile.class).getId()); // The IDs of the covariates must match the ones in the db LambdaSet<Integer> knowIds = knownCovariatesById.keySet(); LambdaSet<Integer> configIds = configCovariatesById.keySet(); if (!knowIds.equals(configIds)) { return false; } // Check Subfiles for (Integer id : knowIds) { CovariateFile knownCovariate = knownCovariatesById.get(id); JsonCovariateFile configCovariate = configCovariatesById.get(id); if (!checkSubFile(knownCovariate, configCovariate)) { return false; } } return true; } private boolean checkSubFile(CovariateFile knownCovariate, JsonCovariateFile configCovariate) { List<CovariateSubFile> knownCovariateSubFiles = knownCovariate.getFiles(); List<JsonCovariateSubFile> configCovariateSubFiles = configCovariate.getSubFiles(); // The IDs of the sub files for this covariate must match the ones in the db LambdaList<Integer> knownSubFileIds = with(knownCovariateSubFiles) .extract(on(CovariateSubFile.class).getId()); LambdaList<Integer> configSubFileIds = with(configCovariateSubFiles) .extract(on(JsonCovariateSubFile.class).getId()); if (!knownSubFileIds.equals(configSubFileIds)) { return false; } // The file paths of the sub files for this covariate must match the ones in the db LambdaList<String> knownSubFilePaths = with(knownCovariateSubFiles) .extract(on(CovariateSubFile.class).getFile()); LambdaList<String> configSubFilePaths = with(configCovariateSubFiles) .extract(on(JsonCovariateSubFile.class).getPath()); if (!knownSubFilePaths.equals(configSubFilePaths)) { return false; } return true; } private boolean checkEnabledDiseaseUniqueness(JsonCovariateConfiguration config) { for (JsonCovariateFile file : config.getFiles()) { if (with(file.getEnabled()).distinct().size() != file.getEnabled().size()) { return false; } } return true; } private boolean checkDiseaseReferenceIntegrity(JsonCovariateConfiguration config) { Collection<Integer> knownDiseaseIds = with(diseaseService.getAllDiseaseGroups()).extract(on(DiseaseGroup.class).getId()); Collection<Integer> linkedDiseaseIds = flatten(with(config.getFiles()).extract(on(JsonCovariateFile.class).getEnabled())); return with(linkedDiseaseIds).all(isIn(knownDiseaseIds)); } }