diff --git a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java index b8855462f..4b985decb 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java @@ -44,7 +44,7 @@ public class GraphQLMutation extends BaseGraphQLResource { Logger log = LoggerFactory.getLogger(GraphQLMutation.class); final AuthService authService; - final IRegisterService registerService; + final IRegisterService blockRegisterService; final IRegisterService sectionRegisterService; final PlanService planService; final LabelPrintService labelPrintService; @@ -115,7 +115,7 @@ public class GraphQLMutation extends BaseGraphQLResource { @Autowired public GraphQLMutation(ObjectMapper objectMapper, AuthenticationComponent authComp, AuthService authService, - IRegisterService registerService, + IRegisterService blockRegisterService, IRegisterService sectionRegisterService, PlanService planService, LabelPrintService labelPrintService, ConfirmOperationService confirmOperationService, @@ -150,7 +150,7 @@ public GraphQLMutation(ObjectMapper objectMapper, AuthenticationComponent authCo ProteinPanelAdminService proteinPanelAdminService) { super(objectMapper, authComp, userRepo); this.authService = authService; - this.registerService = registerService; + this.blockRegisterService = blockRegisterService; this.sectionRegisterService = sectionRegisterService; this.planService = planService; this.labelPrintService = labelPrintService; @@ -245,12 +245,12 @@ public DataFetcher userSelfRegister(final User.Role role) { }; } - public DataFetcher register() { + public DataFetcher blockRegister() { return dfe -> { User user = checkUser(dfe, User.Role.normal); - RegisterRequest request = arg(dfe, "request", RegisterRequest.class); - logRequest("Register", user, request); - return registerService.register(user, request); + BlockRegisterRequest request = arg(dfe, "request", BlockRegisterRequest.class); + logRequest("Block register", user, request); + return blockRegisterService.register(user, request); }; } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java index 61ad5d8b8..6b7206940 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java @@ -153,7 +153,7 @@ private RuntimeWiring buildWiring() { .dataFetcher("registerAsEndUser", graphQLMutation.userSelfRegister(User.Role.enduser)) // internal transaction .dataFetcher("login", graphQLMutation.logIn()) .dataFetcher("logout", graphQLMutation.logOut()) - .dataFetcher("register", transact(graphQLMutation.register())) + .dataFetcher("registerBlocks", transact(graphQLMutation.blockRegister())) .dataFetcher("plan", transact(graphQLMutation.recordPlan())) .dataFetcher("printLabware", graphQLMutation.printLabware()) // not transacted .dataFetcher("confirmOperation", transact(graphQLMutation.confirmOperation())) diff --git a/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java b/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java index 0e98fd3c2..80cf81a61 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java @@ -71,7 +71,7 @@ public Validator externalNameValidator() { Set charTypes = EnumSet.of( CharacterType.ALPHA, CharacterType.DIGIT, CharacterType.HYPHEN, CharacterType.UNDERSCORE, CharacterType.FULL_STOP - ) ; + ); return new StringValidator("External identifier", 3, 64, charTypes); } @@ -79,8 +79,8 @@ public Validator externalNameValidator() { public Validator externalBarcodeValidator() { Set charTypes = EnumSet.of( CharacterType.ALPHA, CharacterType.DIGIT, CharacterType.HYPHEN, - CharacterType.UNDERSCORE - ) ; + CharacterType.UNDERSCORE, CharacterType.FULL_STOP + ); return new StringValidator("External barcode", 3, 32, charTypes); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/model/Sample.java b/src/main/java/uk/ac/sanger/sccp/stan/model/Sample.java index 63b6a77b6..55f7bc12f 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/model/Sample.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/model/Sample.java @@ -20,6 +20,7 @@ public class Sample { private Tissue tissue; @ManyToOne private BioState bioState; + private Integer blockHighestSection; public Sample() {} @@ -62,6 +63,24 @@ public void setBioState(BioState bioState) { this.bioState = bioState; } + public Integer getBlockHighestSection() { + return this.blockHighestSection; + } + + public void setBlockHighestSection(Integer blockHighestSection) { + this.blockHighestSection = blockHighestSection; + } + + public boolean isBlock() { + return blockHighestSection != null; + } + + public static Sample newBlock(Integer id, Tissue tissue, BioState bs, Integer blockHighestSection) { + Sample sample = new Sample(id, null, tissue, bs); + sample.setBlockHighestSection(blockHighestSection); + return sample; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -70,7 +89,9 @@ public boolean equals(Object o) { return (Objects.equals(this.id, that.id) && Objects.equals(this.section, that.section) && Objects.equals(this.tissue, that.tissue) - && Objects.equals(this.bioState, that.bioState)); + && Objects.equals(this.bioState, that.bioState) + && Objects.equals(this.blockHighestSection, that.blockHighestSection) + ); } @Override @@ -85,6 +106,7 @@ public String toString() { .addRepr("section", section) .add("tissue", tissue) .add("bioState", bioState) + .addIfNotNull("blockHighestSection", blockHighestSection) .toString(); } } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/model/Slot.java b/src/main/java/uk/ac/sanger/sccp/stan/model/Slot.java index b67684445..d38a69796 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/model/Slot.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/model/Slot.java @@ -12,7 +12,6 @@ * @author dr6 */ @Entity -@SecondaryTable(name = "block_info", pkJoinColumns = @PrimaryKeyJoinColumn(name = "slot_id")) public class Slot { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -25,22 +24,14 @@ public class Slot { @JoinTable(name = "slot_sample", inverseJoinColumns = @JoinColumn(name="sample_id")) private List samples; - @Column(table = "block_info", name = "sample_id") - private Integer blockSampleId; - @Column(table = "block_info", name = "highest_section") - private Integer blockHighestSection; - public Slot() { this.samples = new ArrayList<>(); } - public Slot(Integer id, Integer labwareId, Address address, List samples, Integer blockSampleId, - Integer blockHighestSection) { + public Slot(Integer id, Integer labwareId, Address address, List samples) { this.id = id; this.labwareId = labwareId; this.address = address; - this.blockSampleId = blockSampleId; - this.blockHighestSection = blockHighestSection; setSamples(samples); } @@ -83,24 +74,8 @@ public void addSample(Sample sample) { this.samples.add(sample); } - public Integer getBlockSampleId() { - return this.blockSampleId; - } - - public void setBlockSampleId(Integer blockSampleId) { - this.blockSampleId = blockSampleId; - } - - public Integer getBlockHighestSection() { - return this.blockHighestSection; - } - - public void setBlockHighestSection(Integer blockHighestSection) { - this.blockHighestSection = blockHighestSection; - } - public boolean isBlock() { - return (this.blockSampleId != null); + return (this.samples.stream().anyMatch(Sample::isBlock)); } @Override @@ -112,8 +87,7 @@ public boolean equals(Object o) { && Objects.equals(this.address, that.address) && Objects.equals(this.labwareId, that.labwareId) && Objects.equals(this.samples, that.samples) - && Objects.equals(this.blockSampleId, that.blockSampleId) - && Objects.equals(this.blockHighestSection, that.blockHighestSection)); + ); } @Override @@ -128,8 +102,6 @@ public String toString() { .add("labwareId", labwareId) .add("address", address) .add("samples", samples) - .add("blockSampleId", blockSampleId) - .add("blockHighestSection", blockHighestSection) .toString(); } } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/repo/LabwareRepo.java b/src/main/java/uk/ac/sanger/sccp/stan/repo/LabwareRepo.java index d30cbf259..667a3d736 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/repo/LabwareRepo.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/repo/LabwareRepo.java @@ -24,6 +24,9 @@ default Labware getByBarcode(final String barcode) throws EntityNotFoundExceptio @Query("select barcode from Labware where barcode in (?1)") Set findBarcodesByBarcodeIn(Collection barcodes); + @Query("select externalBarcode from Labware where externalBarcode in (?1)") + Set findExternalBarcodesIn(Collection barcodes); + default Labware getById(final Integer id) throws EntityNotFoundException { return findById(id).orElseThrow(() -> new EntityNotFoundException("No labware found with id "+id)); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/TissueBlockRequest.java b/src/main/java/uk/ac/sanger/sccp/stan/request/TissueBlockRequest.java index 83fe342e9..9b9666939 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/TissueBlockRequest.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/TissueBlockRequest.java @@ -1,10 +1,13 @@ package uk.ac.sanger.sccp.stan.request; +import uk.ac.sanger.sccp.stan.model.Address; import uk.ac.sanger.sccp.utils.BasicUtils; import java.util.List; import java.util.Objects; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullToEmpty; + /** * A request to process original tissue into blocks. * @author dr6 @@ -84,34 +87,85 @@ public int hashCode() { } /** - * The input about a new block being created. + * The input about labware containing new blocks * @author dr6 */ public static class TissueBlockLabware { - private String sourceBarcode; private String labwareType; private String preBarcode; - private Integer commentId; - private String replicate; + private List contents = List.of(); public TissueBlockLabware() {} - public TissueBlockLabware(String sourceBarcode, String labwareType, String replicate) { - this(sourceBarcode, labwareType, replicate, null, null); + /** + * The labware type for the new labware. + */ + public String getLabwareType() { + return this.labwareType; } - public TissueBlockLabware(String sourceBarcode, String labwareType, String replicate, - String preBarcode, Integer commentId) { - this.sourceBarcode = sourceBarcode; + public void setLabwareType(String labwareType) { this.labwareType = labwareType; - this.replicate = replicate; - this.preBarcode = preBarcode; - this.commentId = commentId; } /** - * The original tissue barcode. + * The barcode of the new labware, if it is prebarcoded. */ + public String getPreBarcode() { + return this.preBarcode; + } + + public void setPreBarcode(String preBarcode) { + this.preBarcode = preBarcode; + } + + public List getContents() { + return this.contents; + } + + public void setContents(List contents) { + this.contents = nullToEmpty(contents); + } + + @Override + public String toString() { + return BasicUtils.describe("TissueBlockLabware") + .add("labwareType", labwareType) + .add("preBarcode", preBarcode) + .add("contents", contents) + .reprStringValues() + .omitNullValues() + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TissueBlockLabware that = (TissueBlockLabware) o; + return (Objects.equals(this.labwareType, that.labwareType) + && Objects.equals(this.preBarcode, that.preBarcode) + && Objects.equals(this.contents, that.contents)); + } + + @Override + public int hashCode() { + return Objects.hash(labwareType, preBarcode, contents); + } + } + + /** + * The content of a block-sample + */ + public static class TissueBlockContent { + private String sourceBarcode; + private Integer sourceSampleId; + private List
addresses = List.of(); + private Integer commentId; + private String replicate; + + public TissueBlockContent() {} + public String getSourceBarcode() { return this.sourceBarcode; } @@ -120,31 +174,22 @@ public void setSourceBarcode(String sourceBarcode) { this.sourceBarcode = sourceBarcode; } - /** - * The labware type for the new labware. - */ - public String getLabwareType() { - return this.labwareType; + public Integer getSourceSampleId() { + return this.sourceSampleId; } - public void setLabwareType(String labwareType) { - this.labwareType = labwareType; + public void setSourceSampleId(Integer sourceSampleId) { + this.sourceSampleId = sourceSampleId; } - /** - * The barcode of the new labware, if it is prebarcoded. - */ - public String getPreBarcode() { - return this.preBarcode; + public List
getAddresses() { + return this.addresses; } - public void setPreBarcode(String preBarcode) { - this.preBarcode = preBarcode; + public void setAddresses(List
addresses) { + this.addresses = nullToEmpty(addresses); } - /** - * The comment (if any) associated with this operation. - */ public Integer getCommentId() { return this.commentId; } @@ -153,9 +198,6 @@ public void setCommentId(Integer commentId) { this.commentId = commentId; } - /** - * The replicate number for the new block. - */ public String getReplicate() { return this.replicate; } @@ -166,14 +208,13 @@ public void setReplicate(String replicate) { @Override public String toString() { - return BasicUtils.describe("TissueBlockLabware") + return BasicUtils.describe(this) .add("sourceBarcode", sourceBarcode) - .add("labwareType", labwareType) - .add("preBarcode", preBarcode) + .add("sourceSampleId", sourceSampleId) + .add("addresses", addresses) .add("commentId", commentId) .add("replicate", replicate) .reprStringValues() - .omitNullValues() .toString(); } @@ -181,10 +222,10 @@ public String toString() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - TissueBlockLabware that = (TissueBlockLabware) o; + TissueBlockContent that = (TissueBlockContent) o; return (Objects.equals(this.sourceBarcode, that.sourceBarcode) - && Objects.equals(this.labwareType, that.labwareType) - && Objects.equals(this.preBarcode, that.preBarcode) + && Objects.equals(this.sourceSampleId, that.sourceSampleId) + && Objects.equals(this.addresses, that.addresses) && Objects.equals(this.commentId, that.commentId) && Objects.equals(this.replicate, that.replicate) ); @@ -192,7 +233,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(sourceBarcode, replicate); + return Objects.hash(sourceBarcode, sourceSampleId, addresses, commentId, replicate); } } } \ No newline at end of file diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterLabware.java b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterLabware.java new file mode 100644 index 000000000..768935f24 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterLabware.java @@ -0,0 +1,94 @@ +package uk.ac.sanger.sccp.stan.request.register; + +import java.util.List; +import java.util.Objects; + +import static uk.ac.sanger.sccp.utils.BasicUtils.describe; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullToEmpty; + +/** + * A request to register in a piece of labware containing one or more block-samples. + * @author dr6 + */ +public class BlockRegisterLabware { + private String labwareType; + private String medium; + private String fixative; + private String externalBarcode; + private List samples = List.of(); + + /** The name of the type of labware containing the block. */ + public String getLabwareType() { + return this.labwareType; + } + + public void setLabwareType(String labwareType) { + this.labwareType = labwareType; + } + + /** The medium used for the tissue. */ + public String getMedium() { + return this.medium; + } + + public void setMedium(String medium) { + this.medium = medium; + } + + /** The fixative used for the tissue. */ + public String getFixative() { + return this.fixative; + } + + public void setFixative(String fixative) { + this.fixative = fixative; + } + + /** The external barcode of the labware. */ + public String getExternalBarcode() { + return this.externalBarcode; + } + + public void setExternalBarcode(String externalBarcode) { + this.externalBarcode = externalBarcode; + } + + /** The samples in this block. */ + public List getSamples() { + return this.samples; + } + + public void setSamples(List samples) { + this.samples = nullToEmpty(samples); + } + + @Override + public String toString() { + return describe(this) + .add("labwareType", labwareType) + .add("medium", medium) + .add("fixative", fixative) + .add("externalBarcode", externalBarcode) + .add("samples", samples) + .reprStringValues() + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || o.getClass() != this.getClass()) return false; + BlockRegisterLabware that = (BlockRegisterLabware) o; + return (Objects.equals(this.labwareType, that.labwareType) + && Objects.equals(this.medium, that.medium) + && Objects.equals(this.fixative, that.fixative) + && Objects.equals(this.externalBarcode, that.externalBarcode) + && Objects.equals(this.samples, that.samples) + ); + } + + @Override + public int hashCode() { + return Objects.hash(labwareType, medium, fixative, externalBarcode, samples); + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java index fe0bfeb0a..52a69c9c2 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java @@ -1,211 +1,65 @@ package uk.ac.sanger.sccp.stan.request.register; -import uk.ac.sanger.sccp.stan.model.LifeStage; - -import java.time.LocalDate; +import java.util.List; import java.util.Objects; import static uk.ac.sanger.sccp.utils.BasicUtils.describe; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullToEmpty; /** - * The information required to register a block. + * A request to register new blocks of tissue. * @author dr6 */ public class BlockRegisterRequest { - private String donorIdentifier; - private LifeStage lifeStage; - private String hmdmc; - private String tissueType; - private int spatialLocation; - private String replicateNumber; - private String externalIdentifier; - private int highestSection; - private String labwareType; - private String medium; - private String fixative; - private String species; - private boolean existingTissue; - private LocalDate sampleCollectionDate; - private String bioRiskCode; - private String cellClass; - - public String getDonorIdentifier() { - return this.donorIdentifier; - } - - public void setDonorIdentifier(String donorIdentifier) { - this.donorIdentifier = donorIdentifier; - } - - public LifeStage getLifeStage() { - return this.lifeStage; - } - - public void setLifeStage(LifeStage lifeStage) { - this.lifeStage = lifeStage; - } - - public String getHmdmc() { - return this.hmdmc; - } - - public void setHmdmc(String hmdmc) { - this.hmdmc = hmdmc; - } - - public String getTissueType() { - return this.tissueType; - } - - public void setTissueType(String tissueType) { - this.tissueType = tissueType; - } - - public int getSpatialLocation() { - return this.spatialLocation; - } - - public void setSpatialLocation(int spatialLocation) { - this.spatialLocation = spatialLocation; - } - - public String getReplicateNumber() { - return this.replicateNumber; - } - - public void setReplicateNumber(String replicateNumber) { - this.replicateNumber = replicateNumber; - } - - public String getExternalIdentifier() { - return this.externalIdentifier; - } - - public void setExternalIdentifier(String externalIdentifier) { - this.externalIdentifier = externalIdentifier; - } - - public int getHighestSection() { - return this.highestSection; - } - - public void setHighestSection(int highestSection) { - this.highestSection = highestSection; - } - - public String getLabwareType() { - return this.labwareType; - } - - public void setLabwareType(String labwareType) { - this.labwareType = labwareType; - } - - public String getMedium() { - return this.medium; - } - - public void setMedium(String medium) { - this.medium = medium; - } - - public String getFixative() { - return this.fixative; - } - - public void setFixative(String fixative) { - this.fixative = fixative; - } - - public String getSpecies() { - return this.species; - } - - public void setSpecies(String species) { - this.species = species; - } - - public boolean isExistingTissue() { - return this.existingTissue; - } + private List workNumbers = List.of(); + private List labware = List.of(); - public void setExistingTissue(boolean existingTissue) { - this.existingTissue = existingTissue; - } + public BlockRegisterRequest() {} // required - public LocalDate getSampleCollectionDate() { - return this.sampleCollectionDate; + public BlockRegisterRequest(List workNumbers, List labware) { + setWorkNumbers(workNumbers); + setLabware(labware); } - public void setSampleCollectionDate(LocalDate sampleCollectionDate) { - this.sampleCollectionDate = sampleCollectionDate; + /** The work numbers for the request. */ + public List getWorkNumbers() { + return this.workNumbers; } - public String getBioRiskCode() { - return this.bioRiskCode; + public void setWorkNumbers(List workNumbers) { + this.workNumbers = nullToEmpty(workNumbers); } - public void setBioRiskCode(String bioRiskCode) { - this.bioRiskCode = bioRiskCode; + /** The labware to register. */ + public List getLabware() { + return this.labware; } - public String getCellClass() { - return this.cellClass; + public void setLabware(List labware) { + this.labware = nullToEmpty(labware); } - public void setCellClass(String cellClass) { - this.cellClass = cellClass; + @Override + public String toString() { + return describe(this) + .add("workNumbers", workNumbers) + .add("labware", labware) + .reprStringValues() + .toString(); } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null || o.getClass() != this.getClass()) return false; BlockRegisterRequest that = (BlockRegisterRequest) o; - return (this.spatialLocation == that.spatialLocation - && Objects.equals(this.replicateNumber, that.replicateNumber) - && this.highestSection == that.highestSection - && this.existingTissue == that.existingTissue - && Objects.equals(this.donorIdentifier, that.donorIdentifier) - && this.lifeStage == that.lifeStage - && Objects.equals(this.hmdmc, that.hmdmc) - && Objects.equals(this.tissueType, that.tissueType) - && Objects.equals(this.externalIdentifier, that.externalIdentifier) - && Objects.equals(this.labwareType, that.labwareType) - && Objects.equals(this.medium, that.medium) - && Objects.equals(this.fixative, that.fixative) - && Objects.equals(this.species, that.species) - && Objects.equals(this.sampleCollectionDate, that.sampleCollectionDate) - && Objects.equals(this.bioRiskCode, that.bioRiskCode) - && Objects.equals(this.cellClass, that.cellClass) + return (Objects.equals(this.workNumbers, that.workNumbers) + && Objects.equals(this.labware, that.labware) ); } @Override public int hashCode() { - return (externalIdentifier!=null ? externalIdentifier.hashCode() : 0); - } - - @Override - public String toString() { - return describe(this) - .add("donorIdentifier", donorIdentifier) - .add("lifeStage", lifeStage) - .add("hmdmc", hmdmc) - .add("tissueType", tissueType) - .add("spatialLocation", spatialLocation) - .add("replicateNumber", replicateNumber) - .add("externalIdentifier", externalIdentifier) - .add("highestSection", highestSection) - .add("labwareType", labwareType) - .add("medium", medium) - .add("fixative", fixative) - .add("species", species) - .add("existingTissue", existingTissue) - .add("sampleCollectionDate", sampleCollectionDate) - .add("bioRiskCode", bioRiskCode) - .add("cellClass", cellClass) - .reprStringValues() - .toString(); + return Objects.hash(workNumbers, labware); } -} +} \ No newline at end of file diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterSample.java b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterSample.java new file mode 100644 index 000000000..7525bfe1e --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterSample.java @@ -0,0 +1,208 @@ +package uk.ac.sanger.sccp.stan.request.register; + +import uk.ac.sanger.sccp.stan.model.Address; +import uk.ac.sanger.sccp.stan.model.LifeStage; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +import static uk.ac.sanger.sccp.utils.BasicUtils.describe; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullToEmpty; + +/** + * A sample inside a block being registered. + * @author dr6 + */ +public class BlockRegisterSample { + private List
addresses = List.of(); + private String donorIdentifier; + private LifeStage lifeStage; + private String hmdmc; + private String tissueType; + private Integer spatialLocation; + private String replicateNumber; + private String externalIdentifier; + private Integer highestSection; + private String species; + private String cellClass; + private boolean existingTissue; + private LocalDate sampleCollectionDate; + private String bioRiskCode; + + /** The addresses of slots containing the sample */ + public List
getAddresses() { + return this.addresses; + } + + public void setAddresses(List
addresses) { + this.addresses = nullToEmpty(addresses); + } + + /** The string to use as the donor name. */ + public String getDonorIdentifier() { + return this.donorIdentifier; + } + + public void setDonorIdentifier(String donorIdentifier) { + this.donorIdentifier = donorIdentifier; + } + + /** The life stage of the donor. */ + public LifeStage getLifeStage() { + return this.lifeStage; + } + + public void setLifeStage(LifeStage lifeStage) { + this.lifeStage = lifeStage; + } + + /** The HMDMC to use for the tissue. */ + public String getHmdmc() { + return this.hmdmc; + } + + public void setHmdmc(String hmdmc) { + this.hmdmc = hmdmc; + } + + /** The name of the tissue type (the organ from which the tissue is taken). */ + public String getTissueType() { + return this.tissueType; + } + + public void setTissueType(String tissueType) { + this.tissueType = tissueType; + } + + /** The code for the spatial location from which the tissue is taken. */ + public Integer getSpatialLocation() { + return this.spatialLocation; + } + + public void setSpatialLocation(Integer spatialLocation) { + this.spatialLocation = spatialLocation; + } + + /** The string to use for the replicate number of the tissue. */ + public String getReplicateNumber() { + return this.replicateNumber; + } + + public void setReplicateNumber(String replicateNumber) { + this.replicateNumber = replicateNumber; + } + + /** The external identifier used to identify the tissue. */ + public String getExternalIdentifier() { + return this.externalIdentifier; + } + + public void setExternalIdentifier(String externalIdentifier) { + this.externalIdentifier = externalIdentifier; + } + + /** The highest section already taken from the tissue block. */ + public Integer getHighestSection() { + return this.highestSection; + } + + public void setHighestSection(Integer highestSection) { + this.highestSection = highestSection; + } + + /** The species of the donor. */ + public String getSpecies() { + return this.species; + } + + public void setSpecies(String species) { + this.species = species; + } + + /** The cellular classification of the tissue. */ + public String getCellClass() { + return this.cellClass; + } + + public void setCellClass(String cellClass) { + this.cellClass = cellClass; + } + + /** Is this a new block of tissue already in the application's database? */ + public boolean isExistingTissue() { + return this.existingTissue; + } + + public void setExistingTissue(boolean existingTissue) { + this.existingTissue = existingTissue; + } + + /** The date the original sample was collected, if known. */ + public LocalDate getSampleCollectionDate() { + return this.sampleCollectionDate; + } + + public void setSampleCollectionDate(LocalDate sampleCollectionDate) { + this.sampleCollectionDate = sampleCollectionDate; + } + + /** The biological risk number for this block. */ + public String getBioRiskCode() { + return this.bioRiskCode; + } + + public void setBioRiskCode(String bioRiskCode) { + this.bioRiskCode = bioRiskCode; + } + + @Override + public String toString() { + return describe(this) + .add("addresses", addresses) + .add("donorIdentifier", donorIdentifier) + .add("lifeStage", lifeStage) + .add("hmdmc", hmdmc) + .add("tissueType", tissueType) + .add("spatialLocation", spatialLocation) + .add("replicateNumber", replicateNumber) + .add("externalIdentifier", externalIdentifier) + .add("highestSection", highestSection) + .add("species", species) + .add("cellClass", cellClass) + .add("existingTissue", existingTissue) + .add("sampleCollectionDate", sampleCollectionDate) + .add("bioRiskCode", bioRiskCode) + .reprStringValues() + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || o.getClass() != this.getClass()) return false; + BlockRegisterSample that = (BlockRegisterSample) o; + return (this.existingTissue == that.existingTissue + && Objects.equals(this.addresses, that.addresses) + && Objects.equals(this.donorIdentifier, that.donorIdentifier) + && Objects.equals(this.lifeStage, that.lifeStage) + && Objects.equals(this.hmdmc, that.hmdmc) + && Objects.equals(this.tissueType, that.tissueType) + && Objects.equals(this.spatialLocation, that.spatialLocation) + && Objects.equals(this.replicateNumber, that.replicateNumber) + && Objects.equals(this.externalIdentifier, that.externalIdentifier) + && Objects.equals(this.highestSection, that.highestSection) + && Objects.equals(this.species, that.species) + && Objects.equals(this.cellClass, that.cellClass) + && Objects.equals(this.sampleCollectionDate, that.sampleCollectionDate) + && Objects.equals(this.bioRiskCode, that.bioRiskCode) + ); + } + + @Override + public int hashCode() { + return Objects.hash(addresses, donorIdentifier, lifeStage, hmdmc, tissueType, spatialLocation, replicateNumber, + externalIdentifier, highestSection, species, cellClass, existingTissue, sampleCollectionDate, + bioRiskCode); + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/register/RegisterRequest.java b/src/main/java/uk/ac/sanger/sccp/stan/request/register/RegisterRequest.java deleted file mode 100644 index 635857836..000000000 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/register/RegisterRequest.java +++ /dev/null @@ -1,64 +0,0 @@ -package uk.ac.sanger.sccp.stan.request.register; - -import java.util.List; - -import static uk.ac.sanger.sccp.utils.BasicUtils.describe; - -/** - * The information required to register some blocks. - * @author dr6 - */ -public class RegisterRequest { - private List blocks; - private List workNumbers; - - public RegisterRequest() { - this(null, null); - } - - public RegisterRequest(List blocks) { - this(blocks, null); - } - - public RegisterRequest(List blocks, List workNumbers) { - setBlocks(blocks); - setWorkNumbers(workNumbers); - } - - public List getBlocks() { - return this.blocks; - } - - public void setBlocks(List blocks) { - this.blocks = (blocks==null ? List.of() : blocks); - } - - public List getWorkNumbers() { - return this.workNumbers; - } - - public void setWorkNumbers(List workNumbers) { - this.workNumbers = (workNumbers==null ? List.of() : workNumbers); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RegisterRequest that = (RegisterRequest) o; - return (this.blocks.equals(that.blocks) && this.workNumbers.equals(that.workNumbers)); - } - - @Override - public int hashCode() { - return 31*this.blocks.hashCode() + this.workNumbers.hashCode(); - } - - @Override - public String toString() { - return describe(this) - .add("blocks", blocks) - .add("workNumbers", workNumbers) - .toString(); - } -} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java index 96781ce80..bda611d00 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java @@ -1,80 +1,33 @@ package uk.ac.sanger.sccp.stan.service; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import uk.ac.sanger.sccp.stan.Transactor; -import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.model.User; import uk.ac.sanger.sccp.stan.request.OperationResult; import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; -import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockLabware; +import uk.ac.sanger.sccp.stan.service.block.*; import uk.ac.sanger.sccp.stan.service.store.StoreService; -import uk.ac.sanger.sccp.stan.service.work.WorkService; -import uk.ac.sanger.sccp.utils.BasicUtils; -import uk.ac.sanger.sccp.utils.UCMap; - -import java.util.*; -import java.util.function.Function; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static uk.ac.sanger.sccp.utils.BasicUtils.*; /** * @author dr6 */ @Service public class BlockProcessingServiceImp implements BlockProcessingService { - private final LabwareValidatorFactory lwValFactory; - private final Validator prebarcodeValidator; - private final Validator replicateValidator; - private final LabwareRepo lwRepo; - private final SlotRepo slotRepo; - private final OperationTypeRepo opTypeRepo; - private final OperationCommentRepo opCommentRepo; - private final LabwareTypeRepo ltRepo; - private final BioStateRepo bsRepo; - private final TissueRepo tissueRepo; - private final SampleRepo sampleRepo; - private final MediumRepo mediumRepo; - private final CommentValidationService commentValidationService; - private final OperationService opService; - private final LabwareService lwService; - private final BioRiskService bioRiskService; - private final WorkService workService; + + private final BlockValidatorFactory blockValidatorFactory; + private final BlockMakerFactory blockMakerFactory; private final StoreService storeService; private final Transactor transactor; @Autowired - public BlockProcessingServiceImp(LabwareValidatorFactory lwValFactory, - @Qualifier("tubePrebarcodeValidator") Validator prebarcodeValidator, - @Qualifier("replicateValidator") Validator replicateValidator, - LabwareRepo lwRepo, SlotRepo slotRepo, OperationTypeRepo opTypeRepo, - OperationCommentRepo opCommentRepo, LabwareTypeRepo ltRepo, - BioStateRepo bsRepo, TissueRepo tissueRepo, SampleRepo sampleRepo, MediumRepo mediumRepo, - CommentValidationService commentValidationService, OperationService opService, - LabwareService lwService, BioRiskService bioRiskService, WorkService workService, StoreService storeService, Transactor transactor) { - this.lwValFactory = lwValFactory; - this.prebarcodeValidator = prebarcodeValidator; - this.replicateValidator = replicateValidator; - this.lwRepo = lwRepo; - this.slotRepo = slotRepo; - this.opTypeRepo = opTypeRepo; - this.opCommentRepo = opCommentRepo; - this.ltRepo = ltRepo; - this.bsRepo = bsRepo; - this.tissueRepo = tissueRepo; - this.sampleRepo = sampleRepo; - this.mediumRepo = mediumRepo; - this.commentValidationService = commentValidationService; - this.opService = opService; - this.lwService = lwService; - this.bioRiskService = bioRiskService; - this.workService = workService; + public BlockProcessingServiceImp(BlockValidatorFactory blockValidatorFactory, BlockMakerFactory blockMakerFactory, + StoreService storeService, Transactor transactor) { + this.blockValidatorFactory = blockValidatorFactory; + this.blockMakerFactory = blockMakerFactory; this.storeService = storeService; this.transactor = transactor; } @@ -91,476 +44,12 @@ public OperationResult perform(User user, TissueBlockRequest request) throws Val public OperationResult performInsideTransaction(User user, TissueBlockRequest request) throws ValidationException { requireNonNull(user, "User is null."); requireNonNull(request, "Request is null."); - Collection problems = new LinkedHashSet<>(); - if (request.getLabware().isEmpty()) { - problems.add("No labware specified in request."); - throw new ValidationException("The request could not be validated.", problems); - } - UCMap sources = loadSources(problems, request); - Work work = workService.validateUsableWork(problems, request.getWorkNumber()); - UCMap lwTypes = loadEntities(problems, request, TissueBlockLabware::getLabwareType, - "Labware type", LabwareType::getName, true, ltRepo::findAllByNameIn); - checkPrebarcodes(problems, request, lwTypes); - Map comments = loadComments(problems, request); - checkReplicates(problems, request, sources); - checkDiscardBarcodes(problems, request); - Medium oct = null; - if (lwTypes.values().stream().anyMatch(lt -> lt.getName().equalsIgnoreCase(LabwareType.PROVIASETTE_NAME))) { - oct = mediumRepo.findByName("OCT").orElse(null); - if (oct==null) { - problems.add("OCT medium not found in database."); - } - } - if (!problems.isEmpty()) { - throw new ValidationException("The request could not be validated.", problems); - } - - List samples = createSamples(request, sources, oct); - List destinations = createDestinations(request, samples, lwTypes); - List ops = createOperations(request, user, sources, destinations, comments); - if (work!=null) { - workService.link(work, ops); - } - bioRiskService.copyOpSampleBioRisks(ops); - discardSources(request.getDiscardSourceBarcodes(), sources); - - return new OperationResult(ops, destinations); - } - - /** - * Loads the source labware for the request, and checks they are suitable - * @param problems receptacle for problems found - * @param request the request - * @return the loaded labware, mapped from barcodes - */ - public UCMap loadSources(Collection problems, TissueBlockRequest request) { - BioState bs = bsRepo.getByName("Original sample"); - Set barcodes = new HashSet<>(); - boolean anyMissing = false; - for (var block : request.getLabware()) { - String barcode = block.getSourceBarcode(); - if (nullOrEmpty(barcode)) { - anyMissing = true; - } else { - barcodes.add(barcode.toUpperCase()); - } - } - LabwareValidator val = lwValFactory.getValidator(); - if (anyMissing) { - problems.add("Source barcode missing."); - } - val.loadLabware(lwRepo, barcodes); - val.setSingleSample(true); - val.validateSources(); - val.validateBioState(bs); - problems.addAll(val.getErrors()); - - return UCMap.from(val.getLabware(), Labware::getBarcode); - } - - /** - * Checks that the prebarcodes look appropriate. - * Prebarcodes should be given for labware types that require it; and not given for labware types - * that do not require it. - * Prebarcodes should be of the expected format. - * Prebarcodes should be unique. - * @param problems receptacle for problems found - * @param request the request - * @param lwTypes the map to look up labware types - */ - public void checkPrebarcodes(Collection problems, TissueBlockRequest request, UCMap lwTypes) { - Set missingBarcodeForLwTypes = new LinkedHashSet<>(); - Set unexpectedBarcodeForLwTypes = new LinkedHashSet<>(); - Set prebarcodes = new HashSet<>(); - Set dupes = new LinkedHashSet<>(); - for (TissueBlockLabware block : request.getLabware()) { - LabwareType lt = lwTypes.get(block.getLabwareType()); - String barcode = block.getPreBarcode(); - if (nullOrEmpty(barcode)) { - if (lt!=null && lt.isPrebarcoded()) { - missingBarcodeForLwTypes.add(lt.getName()); - } - } else { - prebarcodeValidator.validate(barcode, problems::add); - if (lt!=null && !lt.isPrebarcoded()) { - unexpectedBarcodeForLwTypes.add(lt.getName()); - } - barcode = barcode.toUpperCase(); - if (!prebarcodes.add(barcode)) { - dupes.add(barcode); - } - } - } - if (!missingBarcodeForLwTypes.isEmpty()) { - problems.add(pluralise("A barcode is required for labware type{s} ", missingBarcodeForLwTypes.size()) + - missingBarcodeForLwTypes + "."); - } - if (!unexpectedBarcodeForLwTypes.isEmpty()) { - problems.add(pluralise("A barcode is not expected for labware type{s} ", unexpectedBarcodeForLwTypes.size()) + - unexpectedBarcodeForLwTypes + "."); - } - if (!dupes.isEmpty()) { - problems.add("Barcode specified multiple times: "+dupes); - } - if (!prebarcodes.isEmpty()) { - Collection existing = lwRepo.findByBarcodeIn(prebarcodes); - if (!existing.isEmpty()) { - List existingBarcodes = existing.stream() - .map(Labware::getBarcode) - .toList(); - problems.add("Barcode already in use: "+existingBarcodes); - } else { - existing = lwRepo.findByExternalBarcodeIn(prebarcodes); - if (!existing.isEmpty()) { - List existingBarcodes = existing.stream() - .map(Labware::getExternalBarcode) - .toList(); - problems.add("External barcode already in use: " + existingBarcodes); - } - } - } - } - - /** - * Validates and loads comments. Comments are optional, so null comment ids are ignored. - * @param problems receptacle for problems - * @param request the request - * @return a map of comments from their ids - */ - public Map loadComments(Collection problems, TissueBlockRequest request) { - Collection comments = commentValidationService.validateCommentIds(problems, - request.getLabware().stream() - .map(TissueBlockLabware::getCommentId) - .filter(Objects::nonNull) - ); - if (comments.isEmpty()) { - return Map.of(); - } - return comments.stream().collect(BasicUtils.inMap(Comment::getId)); - } - - /** - * Loads entities specified by a string field. - * @param problems receptacle for problems - * @param request the request - * @param fieldGetter the function to get the string from the block in the request - * @param fieldName the name of the field (for problem messages) - * @param entityFieldGetter the function to get the string from the entity - * @param required whether it is a problem for a string to be missing (null or empty) - * @param lookup function to look up matching entities (collectively) from a collection - * @return a map of the entities found from the strings values for the specified field - * @param the type of entity - */ - public UCMap loadEntities(Collection problems, TissueBlockRequest request, - Function fieldGetter, String fieldName, - Function entityFieldGetter, - boolean required, Function, ? extends Collection> lookup) { - boolean anyMissing = false; - Set fieldValues = new LinkedHashSet<>(); - for (var block : request.getLabware()) { - String value = fieldGetter.apply(block); - if (nullOrEmpty(value)) { - anyMissing = true; - } else { - fieldValues.add(value); - } - } - if (anyMissing && required) { - problems.add(fieldName+" missing."); - } - if (fieldValues.isEmpty()) { - return new UCMap<>(); - } - UCMap entities = UCMap.from(lookup.apply(fieldValues), entityFieldGetter); - List unknown = fieldValues.stream() - .filter(value -> entities.get(value)==null) - .filter(distinctUCSerial()) - .map(BasicUtils::repr) - .toList(); - if (!unknown.isEmpty()) { - problems.add(fieldName+" unknown: "+unknown); - } - return entities; - } - - /** - * Checks the formatting of the replicate field, if provided; otherwise, assigns the same replicate as the source. - * @param problems receptacle for problems - * @param request the request - * @param sources map to look up source labware from its barcode - */ - public void checkReplicates(Collection problems, TissueBlockRequest request, UCMap sources) { - boolean anyMissing = false; - boolean differentRep = false; - Set repKeys = new LinkedHashSet<>(); - Set repKeyDupes = new LinkedHashSet<>(); - for (var block : request.getLabware()) { - String sourceReplicateNumber = Optional.ofNullable(sources.get(block.getSourceBarcode())) - .flatMap(BlockProcessingServiceImp::getSample) - .map(sam -> sam.getTissue().getReplicate()) - .orElse(null); - if (nullOrEmpty(sourceReplicateNumber)) { - if (nullOrEmpty(block.getReplicate())) { - anyMissing = true; - } else if (replicateValidator.validate(block.getReplicate(), problems::add)) { - RepKey repKey = RepKey.from(sources.get(block.getSourceBarcode()), block.getReplicate()); - if (repKey != null && !repKeys.add(repKey)) { - repKeyDupes.add(repKey); - } - } - } else if (nullOrEmpty(block.getReplicate())) { - block.setReplicate(sourceReplicateNumber); - } else if (!block.getReplicate().equalsIgnoreCase(sourceReplicateNumber)) { - differentRep = true; - } - } - if (!repKeyDupes.isEmpty()) { - problems.add("Same replicate specified multiple times: "+repKeyDupes); - } - if (anyMissing) { - problems.add("Missing replicate for some blocks."); - } - if (differentRep) { - problems.add("Replicate numbers must match the source replicate number where present."); - } - List alreadyExistRepKeys = repKeys.stream() - .filter(rp -> !tissueRepo.findByDonorIdAndSpatialLocationIdAndReplicate( - rp.donorId(), rp.spatialLocationId(), rp.replicate()).isEmpty() - ).toList(); - if (!alreadyExistRepKeys.isEmpty()) { - problems.add("Replicate already exists in the database: "+alreadyExistRepKeys); - } - } - - /** - * Checks that the barcodes specified to discard are all source barcodes in the request. - * @param problems receptacle for problems - * @param request the request - */ - public void checkDiscardBarcodes(Collection problems, TissueBlockRequest request) { - if (request.getDiscardSourceBarcodes().isEmpty()) { - return; - } - Set sourceBarcodes = request.getLabware().stream() - .map(TissueBlockLabware::getSourceBarcode) - .filter(s -> !nullOrEmpty(s)) - .map(String::toUpperCase) - .collect(toSet()); - boolean anyNull = false; - Set missingBarcodes = new LinkedHashSet<>(); - for (String barcode : request.getDiscardSourceBarcodes()) { - if (nullOrEmpty(barcode)) { - anyNull = true; - } else if (!sourceBarcodes.contains(barcode.toUpperCase())) { - missingBarcodes.add(repr(barcode)); - } - } - if (anyNull) { - problems.add("A null or empty string was supplied as a barcode to discard."); - } - if (!missingBarcodes.isEmpty()) { - problems.add(pluralise("The given list of barcodes to discard includes {a |}barcode{s} " + - "that {is|are} not specified as {a |}source barcode{s} in this request: ", missingBarcodes.size()) - + missingBarcodes); - } - } - - /** - * Creates new samples, in a list parallel to the blocks specified in the request. - * @param request the request - * @param sources the source labware - * @param oct the oct medium for where it is required - * @return a list of samples, corresponding to the respective blocks in the request - */ - public List createSamples(TissueBlockRequest request, UCMap sources, Medium oct) { - final BioState bs = bsRepo.getByName("Tissue"); - return request.getLabware() - .stream() - .map(block -> createSample(block, sources.get(block.getSourceBarcode()), bs, - (block.getLabwareType().equalsIgnoreCase(LabwareType.PROVIASETTE_NAME)) ? oct : null)) - .collect(toList()); - } - - /** - * Creates a new derived tissue from the original; or returns the original if nothing needs changing. - * @param original the original tissue - * @param replicate the specified replicate number - * @param medium the specified medium - * @return a new or original tissue - */ - public Tissue getOrCreateTissue(Tissue original, String replicate, Medium medium) { - boolean changeReplicate = (!nullOrEmpty(replicate) && !replicate.equalsIgnoreCase(original.getReplicate())); - boolean changeMedium = (medium!=null && !medium.getId().equals(original.getMedium().getId())); - if (!changeReplicate && !changeMedium) { - return original; - } - Tissue tissue = original.derived(); - if (changeReplicate) { - tissue.setReplicate(replicate.toLowerCase()); - } - if (changeMedium) { - tissue.setMedium(medium); - } - return tissueRepo.save(tissue); - } - - /** - * Creates a new sample, from a new tissue if necessary. - * @param block the specification of the block - * @param sourceLabware the source labware for the new block - * @param bs the bio state for the new block - * @param medium the medium for the new block, if specified - * @return the newly created sample - */ - public Sample createSample(TissueBlockLabware block, Labware sourceLabware, BioState bs, Medium medium) { - Tissue original = getSample(sourceLabware) - .map(Sample::getTissue) - .orElseThrow(); - Tissue tissue = getOrCreateTissue(original, block.getReplicate(), medium); - return sampleRepo.save(new Sample(null, null, tissue, bs)); - } - - /** - * Creates destinations for the given request. - * @param request the request - * @param samples the samples for the respective blocks of the request - * @param lwTypes map to look up labware types by name - * @return a list of labware for respective blocks of the request - */ - public List createDestinations(TissueBlockRequest request, List samples, UCMap lwTypes) { - final var sampleIter = samples.iterator(); - return request.getLabware().stream() - .map(block -> createDestination(lwTypes.get(block.getLabwareType()), sampleIter.next(), block.getPreBarcode())) - .collect(toList()); - } - - /** - * Creates labware containing a sample. - * @param lwType the type of labware - * @param sample the sample the labware will contain in its first slot - * @param preBarcode the prebarcode (if any), or null - * @return the newly created labware, containing the sample - */ - public Labware createDestination(LabwareType lwType, Sample sample, String preBarcode) { - Labware lw = lwService.create(lwType, preBarcode, preBarcode); - Slot slot = lw.getFirstSlot(); - slot.addSample(sample); - slot.setBlockSampleId(sample.getId()); - slot.setBlockHighestSection(0); - slotRepo.save(slot); - return lw; - } - - /** - * Creates block processing operations. - * @param request the block processing request - * @param user the user responsible for the operations - * @param sources map to look up source labware by its barcode - * @param destinations the labware for the respective blocks of the request - * @param comments map to look up comments by their id - * @return the operations for the respective blocks in the request - */ - public List createOperations(TissueBlockRequest request, User user, - UCMap sources, List destinations, - Map comments) { - final OperationType opType = opTypeRepo.getByName("Block processing"); - final var destIter = destinations.iterator(); - return request.getLabware().stream() - .map(block -> createOperation(opType, user, sources.get(block.getSourceBarcode()), destIter.next(), - block.getCommentId()==null ? null : comments.get(block.getCommentId()))) - .collect(toList()); - } - - /** - * Creates an operation as specified - * @param opType the type of op - * @param user the user responsible for the op - * @param sourceLw the source labware (containing one sample in one slot) - * @param destLw the destination labware (containing one sample in its first slot) - * @param comment the comment to record (if any), or null - * @return the created operation - */ - public Operation createOperation(OperationType opType, User user, - Labware sourceLw, Labware destLw, Comment comment) { - Slot src = sourceLw.getSlots().stream() - .filter(slot -> !slot.getSamples().isEmpty()) - .findAny() - .orElseThrow(); - Slot dest = destLw.getFirstSlot(); - Sample srcSample = src.getSamples().getFirst(); - Sample dstSample = dest.getSamples().getFirst(); - Action action = new Action(null, null, src, dest, dstSample, srcSample); - Operation op = opService.createOperation(opType, user, List.of(action), null); - if (comment!=null) { - opCommentRepo.save(new OperationComment(null, comment, op.getId(), dstSample.getId(), dest.getId(), null)); - } - return op; - } - - /** - * Discards the indicated labware - * @param barcodes barcodes of labware to discard - * @param lwMap map to look up labware by its barcode - */ - public void discardSources(Collection barcodes, UCMap lwMap) { - if (!barcodes.isEmpty()) { - List labwareToSave = new ArrayList<>(barcodes.size()); - for (String bc : barcodes) { - Labware lw = lwMap.get(bc); - if (!lw.isDiscarded()) { - lw.setDiscarded(true); - labwareToSave.add(lw); - } - } - lwRepo.saveAll(labwareToSave); - } - } - - /** The unique fields associated with a block */ - record RepKey(Donor donor, SpatialLocation spatialLocation, String replicate) { - RepKey(Donor donor, SpatialLocation spatialLocation, String replicate) { - this.donor = donor; - this.spatialLocation = spatialLocation; - this.replicate = replicate.toLowerCase(); - } - - Integer donorId() { - return this.donor.getId(); - } - Integer spatialLocationId() { - return this.spatialLocation.getId(); - } - - static RepKey from(Labware sourceLabware, String replicate) { - if (sourceLabware==null) { - return null; - } - Tissue tissue = getSample(sourceLabware) - .map(Sample::getTissue) - .orElse(null); - if (tissue==null) { - return null; - } - return new RepKey(tissue.getDonor(), tissue.getSpatialLocation(), replicate); - } - - @NotNull - @Override - public String toString() { - return String.format("{Donor: %s, Tissue type: %s, Spatial location: %s, Replicate: %s}", - donor.getDonorName(), spatialLocation.getTissueType().getName(), spatialLocation.getCode(), - replicate); - } - } - /** - * Helper function to get the sample from a piece of labware. - * @param lw the piece of labware - * @return an optional sample contained in the labware - */ - public static Optional getSample(Labware lw) { - return lw.getSlots().stream() - .flatMap(slot -> slot.getSamples().stream()) - .findAny(); + BlockValidator val = blockValidatorFactory.createBlockValidator(request); + val.validate(); + val.raiseError(); + BlockMaker maker = blockMakerFactory.createBlockMaker(request, val.getLwData(), val.getMedium(), + val.getBioState(), val.getWork(), val.getOpType(), user); + return maker.record(); } } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/LabwareService.java b/src/main/java/uk/ac/sanger/sccp/stan/service/LabwareService.java index 1a69f1712..514f71104 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/LabwareService.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/LabwareService.java @@ -13,6 +13,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static uk.ac.sanger.sccp.utils.BasicUtils.repr; +import static uk.ac.sanger.sccp.utils.BasicUtils.stream; /** * Service to help with labware, including creating labware with appropriate slots. @@ -90,20 +91,20 @@ public List create(LabwareType labwareType, int number) { List barcodes = barcodeIntRepo.createStanBarcodes(number); List newLabware = barcodes.stream() .map(bc -> new Labware(null, bc, labwareType, null)) - .collect(toList()); + .toList(); Iterable savedLabware = labwareRepo.saveAll(newLabware); final int numRows = labwareType.getNumRows(); final int numColumns = labwareType.getNumColumns(); - final List newSlots = Streams.stream(savedLabware).flatMap(lw -> - Address.stream(numRows, numColumns).map(address -> - new Slot(null, lw.getId(), address, null, null, null) - ) - ).collect(toList()); + final List newSlots = stream(savedLabware).flatMap(lw -> { + assert lw != null; + return Address.stream(numRows, numColumns).map(address -> + new Slot(null, lw.getId(), address, null)); + }).toList(); slotRepo.saveAll(newSlots); return Streams.stream(savedLabware) .peek(entityManager::refresh) - .collect(toList()); + .toList(); } /** @@ -121,7 +122,7 @@ public Labware create(Labware unsaved) { final int numRows = labwareType.getNumRows(); final int numColumns = labwareType.getNumColumns(); List newSlots = Address.stream(numRows, numColumns) - .map(address -> new Slot(null, labware.getId(), address, null, null, null)) + .map(address -> new Slot(null, labware.getId(), address, null)) .collect(toList()); slotRepo.saveAll(newSlots); entityManager.refresh(labware); diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/ParaffinProcessingServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/ParaffinProcessingServiceImp.java index b02d116e1..f81b8b2ce 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/ParaffinProcessingServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/ParaffinProcessingServiceImp.java @@ -26,15 +26,16 @@ public class ParaffinProcessingServiceImp implements ParaffinProcessingService { private final OperationTypeRepo opTypeRepo; private final MediumRepo mediumRepo; private final TissueRepo tissueRepo; + private final SlotRepo slotRepo; + private final SampleRepo sampleRepo; private final WorkService workService; private final OperationService opService; private final CommentValidationService commentValidationService; private final LabwareValidatorFactory lwValFactory; - private final SlotRepo slotRepo; @Autowired public ParaffinProcessingServiceImp(LabwareRepo lwRepo, OperationCommentRepo opComRepo, OperationTypeRepo opTypeRepo, - MediumRepo mediumRepo, TissueRepo tissueRepo, SlotRepo slotRepo, + MediumRepo mediumRepo, TissueRepo tissueRepo, SlotRepo slotRepo, SampleRepo sampleRepo, WorkService workService, OperationService opService, CommentValidationService commentValidationService, LabwareValidatorFactory lwValFactory) { @@ -44,6 +45,7 @@ public ParaffinProcessingServiceImp(LabwareRepo lwRepo, OperationCommentRepo opC this.mediumRepo = mediumRepo; this.tissueRepo = tissueRepo; this.slotRepo = slotRepo; + this.sampleRepo = sampleRepo; this.workService = workService; this.opService = opService; this.commentValidationService = commentValidationService; @@ -150,8 +152,8 @@ public Medium loadMedium(Collection problems, String mediumName) { */ public OperationResult record(User user, Collection labware, Work work, Comment comment, Medium medium) { updateMedium(labware, medium); - List ops = createOps(user, labware); - createBlocks(labware); + List changes = createBlocks(labware); + List ops = createOps(user, changes); workService.link(work, ops); recordComments(comment, ops); return new OperationResult(ops, labware); @@ -180,32 +182,80 @@ public void updateMedium(Collection labware, Medium medium) { * Converts labware to blocks, if they aren't already * @param labware the labware to make into blocks, if they aren't already */ - public void createBlocks(Collection labware) { + List createBlocks(Collection labware) { + List samplesToSave = new ArrayList<>(); List slotsToSave = new ArrayList<>(); + List changes = new ArrayList<>(labware.size()); for (Labware lw : labware) { - Slot slot = lw.getFirstSlot(); - if (!slot.isBlock()) { - slot.setBlockSampleId(slot.getSamples().getFirst().getId()); - slot.setBlockHighestSection(0); - slotsToSave.add(slot); + Map newSampleMap = new HashMap<>(); + for (Slot slot : lw.getSlots()) { + if (slot.getSamples().isEmpty()) { + continue; + } + boolean changed = false; + List newSamples = new ArrayList<>(slot.getSamples().size()); + for (Sample sam : slot.getSamples()) { + Sample blockSample; + if (!sam.isBlock()) { + blockSample = newSampleMap.get(sam); + if (blockSample==null) { + blockSample = Sample.newBlock(null, sam.getTissue(), sam.getBioState(), 0); + newSampleMap.put(sam, blockSample); + } + changed = true; + } else { + blockSample = sam; + } + newSamples.add(blockSample); + changes.add(new BlockChange(slot, sam, blockSample)); + } + if (changed) { + slot.setSamples(newSamples); + slotsToSave.add(slot); + } } + samplesToSave.addAll(newSampleMap.values()); + } + if (!samplesToSave.isEmpty()) { + // This should update the sample objects with ids + sampleRepo.saveAll(samplesToSave); } if (!slotsToSave.isEmpty()) { slotRepo.saveAll(slotsToSave); } + return changes; } /** - * Creates operations on the given labware + * Creates operations * @param user user responsible - * @param labware the labware + * @param changes the slots and affected samples * @return the operations created */ - public List createOps(User user, Collection labware) { + List createOps(User user, Collection changes) { OperationType opType = opTypeRepo.getByName("Paraffin processing"); - return labware.stream() - .map(lw -> opService.createOperationInPlace(opType, user, lw, null, null)) - .collect(toList()); + Map> lwChanges = new LinkedHashMap<>(); + for (BlockChange change : changes) { + Integer lwId = change.slot.getLabwareId(); + lwChanges.computeIfAbsent(lwId, id -> new ArrayList<>()).add(change); + } + return lwChanges.values().stream() + .map(cs -> createOp(user, opType, cs)) + .toList(); + } + + /** + * Creates an operation describing the given changes + * @param user the user responsible + * @param opType the operation type + * @param changes slots and samples + * @return the created operation + */ + Operation createOp(User user, OperationType opType, Collection changes) { + List actions = changes.stream() + .map(c -> new Action(null, null, c.slot, c.slot, c.blockSample, c.originalSample)) + .toList(); + return opService.createOperation(opType, user, actions, null); } /** @@ -220,4 +270,7 @@ public void recordComments(Comment comment, Collection ops) { .collect(toList()); opComRepo.saveAll(opComs); } + + /** A slot and the samples involved in its action */ + record BlockChange(Slot slot, Sample originalSample, Sample blockSample) {} } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/SampleService.java b/src/main/java/uk/ac/sanger/sccp/stan/service/SampleService.java deleted file mode 100644 index e0d902417..000000000 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/SampleService.java +++ /dev/null @@ -1,42 +0,0 @@ -package uk.ac.sanger.sccp.stan.service; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import uk.ac.sanger.sccp.stan.model.Sample; -import uk.ac.sanger.sccp.stan.model.Slot; -import uk.ac.sanger.sccp.stan.repo.PlanActionRepo; -import uk.ac.sanger.sccp.stan.repo.SlotRepo; - -/** - * @author dr6 - */ -@Service -public class SampleService { - private final PlanActionRepo planActionRepo; - private final SlotRepo slotRepo; - - @Autowired - public SampleService(PlanActionRepo planActionRepo, SlotRepo slotRepo) { - this.planActionRepo = planActionRepo; - this.slotRepo = slotRepo; - } - - /** - * Figures out the next section number from the given block, and increments its section counter. - * @param block a slot containing a block - * @return the next section number for the given block - */ - public int nextSection(Slot block) { - assert block.isBlock(); - assert block.getSamples().size()==1; - final Sample sample = block.getSamples().get(0); - Integer listedMaxSection = block.getBlockHighestSection(); - int maxSection = (listedMaxSection==null ? 0 : listedMaxSection); - int maxPlanSection = planActionRepo.findMaxPlannedSectionFromSlotId(block.getId()).orElse(0); - int nextSection = Math.max(maxSection, maxPlanSection) + 1; - block.setBlockHighestSection(nextSection); - block.setBlockSampleId(sample.getId()); - slotRepo.save(block); - return nextSection; - } -} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/UnreleaseServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/UnreleaseServiceImp.java index 21a757a54..6a158f620 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/UnreleaseServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/UnreleaseServiceImp.java @@ -23,18 +23,18 @@ public class UnreleaseServiceImp implements UnreleaseService { private final LabwareValidatorFactory labwareValidatorFactory; private final LabwareRepo lwRepo; - private final SlotRepo slotRepo; + private final SampleRepo sampleRepo; private final OperationTypeRepo opTypeRepo; private final OperationService opService; private final WorkService workService; @Autowired public UnreleaseServiceImp(LabwareValidatorFactory labwareValidatorFactory, - LabwareRepo lwRepo, SlotRepo slotRepo, OperationTypeRepo opTypeRepo, + LabwareRepo lwRepo, SampleRepo sampleRepo, OperationTypeRepo opTypeRepo, OperationService opService, WorkService workService) { this.labwareValidatorFactory = labwareValidatorFactory; this.lwRepo = lwRepo; - this.slotRepo = slotRepo; + this.sampleRepo = sampleRepo; this.opTypeRepo = opTypeRepo; this.opService = opService; this.workService = workService; @@ -149,18 +149,22 @@ public void validateRequest(Collection problems, UCMap labware, * @return a string indicating the problem; null if no problem was found. */ public String highestSectionProblem(Labware lw, int highestSection) { - Slot slot = lw.getFirstSlot(); - if (!slot.isBlock()) { + Integer oldValue = lw.getSlots().stream() + .flatMap(slot -> slot.getSamples().stream()) + .map(Sample::getBlockHighestSection) + .filter(Objects::nonNull) + .max(Comparator.naturalOrder()) + .orElse(null); + if (oldValue == null) { return "Cannot set the highest section number from labware "+lw.getBarcode()+" because it is not a block."; } - Integer oldValue = slot.getBlockHighestSection(); - if (oldValue!=null && oldValue > highestSection) { - return String.format("For block %s, cannot reduce the highest section number from %s to %s.", - lw.getBarcode(), slot.getBlockHighestSection(), highestSection); - } if (highestSection < 0) { return "Cannot set the highest section to a negative number."; } + if (oldValue > highestSection) { + return String.format("For block %s, cannot reduce the highest section number from %s to %s.", + lw.getBarcode(), oldValue, highestSection); + } return null; } @@ -191,23 +195,26 @@ public OperationResult perform(User user, UnreleaseRequest request, OperationTyp @NotNull public List updateLabware(List requestLabware, UCMap labwareMap) { List labwareList = new ArrayList<>(requestLabware.size()); - List slotsToUpdate = new ArrayList<>(requestLabware.size()); + Set samplesToUpdate = new HashSet<>(); for (UnreleaseLabware ul : requestLabware) { Labware lw = labwareMap.get(ul.getBarcode()); lw.setReleased(false); if (ul.getHighestSection()!=null) { - Slot slot = lw.getFirstSlot(); - if (slot.getBlockHighestSection()==null || slot.getBlockHighestSection() < ul.getHighestSection()) { - slot.setBlockHighestSection(ul.getHighestSection()); - slotsToUpdate.add(slot); + for (Slot slot : lw.getSlots()) { + for (Sample sample : slot.getSamples()) { + if (sample.isBlock() && !sample.getBlockHighestSection().equals(ul.getHighestSection())) { + sample.setBlockHighestSection(ul.getHighestSection()); + samplesToUpdate.add(sample); + } + } } } labwareList.add(lw); } - if (!slotsToUpdate.isEmpty()) { - slotRepo.saveAll(slotsToUpdate); + if (!samplesToUpdate.isEmpty()) { + sampleRepo.saveAll(new ArrayList<>(samplesToUpdate)); } lwRepo.saveAll(labwareList); return labwareList; diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockData.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockData.java new file mode 100644 index 000000000..d0d0abb76 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockData.java @@ -0,0 +1,111 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockContent; +import uk.ac.sanger.sccp.utils.BasicUtils; + +import java.util.List; +import java.util.Objects; + +/** + * @author dr6 + */ +public class BlockData { + private TissueBlockContent requestContent; + private Labware sourceLabware; + private Sample sourceSample; + private Sample sample; + private List sourceSlots; + private List destSlots; + private Comment comment; + + public BlockData(TissueBlockContent requestContent) { + this.requestContent = requestContent; + } + + public TissueBlockContent getRequestContent() { + return this.requestContent; + } + + public void setRequestContent(TissueBlockContent requestContent) { + this.requestContent = requestContent; + } + + public Labware getSourceLabware() { + return this.sourceLabware; + } + + public void setSourceLabware(Labware sourceLabware) { + this.sourceLabware = sourceLabware; + } + + public Sample getSourceSample() { + return this.sourceSample; + } + + public void setSourceSample(Sample sourceSample) { + this.sourceSample = sourceSample; + } + + public Sample getSample() { + return this.sample; + } + + public void setSample(Sample sample) { + this.sample = sample; + } + + public List getSourceSlots() { + return this.sourceSlots; + } + + public void setSourceSlots(List sourceSlots) { + this.sourceSlots = sourceSlots; + } + + public List getDestSlots() { + return this.destSlots; + } + + public void setDestSlots(List destSlots) { + this.destSlots = destSlots; + } + + public Comment getComment() { + return this.comment; + } + + public void setComment(Comment comment) { + this.comment = comment; + } + + @Override + public String toString() { + return BasicUtils.describe(this) + .add("requestContent", requestContent) + .add("sourceSample", sourceSample) + .add("sample", sample) + .add("sourceSlots", sourceSlots) + .add("destSlots", destSlots) + .add("comment", comment) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BlockData that = (BlockData) o; + return (Objects.equals(this.requestContent, that.requestContent) + && Objects.equals(this.sourceSample, that.sourceSample) + && Objects.equals(this.sample, that.sample) + && Objects.equals(this.sourceSlots, that.sourceSlots) + && Objects.equals(this.destSlots, that.destSlots) + && Objects.equals(this.comment, that.comment) + ); + } + + @Override + public int hashCode() { + return requestContent.hashCode(); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockLabwareData.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockLabwareData.java new file mode 100644 index 000000000..8be4e82a0 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockLabwareData.java @@ -0,0 +1,70 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import uk.ac.sanger.sccp.stan.model.Labware; +import uk.ac.sanger.sccp.stan.model.LabwareType; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockLabware; + +import java.util.List; +import java.util.Objects; + +/** + * @author dr6 + */ +public class BlockLabwareData { + private TissueBlockLabware requestLabware; + private LabwareType lwType; + private Labware labware; + private List blocks; + + public BlockLabwareData(TissueBlockLabware requestLabware) { + this.requestLabware = requestLabware; + this.blocks = requestLabware.getContents().stream().map(BlockData::new).toList(); + } + + public TissueBlockLabware getRequestLabware() { + return this.requestLabware; + } + + public void setRequestLabware(TissueBlockLabware requestLabware) { + this.requestLabware = requestLabware; + } + + public LabwareType getLwType() { + return this.lwType; + } + + public void setLwType(LabwareType lwType) { + this.lwType = lwType; + } + + public Labware getLabware() { + return this.labware; + } + + public void setLabware(Labware labware) { + this.labware = labware; + } + + public List getBlocks() { + return this.blocks; + } + + public void setBlocks(List blocks) { + this.blocks = blocks; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BlockLabwareData that = (BlockLabwareData) o; + return (Objects.equals(this.requestLabware, that.requestLabware) + && Objects.equals(this.lwType, that.lwType) + && Objects.equals(this.labware, that.labware) + && Objects.equals(this.blocks, that.blocks)); + } + + @Override + public int hashCode() { + return requestLabware.hashCode(); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMaker.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMaker.java new file mode 100644 index 000000000..76f8157fe --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMaker.java @@ -0,0 +1,12 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import uk.ac.sanger.sccp.stan.request.OperationResult; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; + +/** + * Utility for executing a {@link TissueBlockRequest} + */ +public interface BlockMaker { + /** Executes the request */ + OperationResult record(); +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMakerFactory.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMakerFactory.java new file mode 100644 index 000000000..82785e717 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMakerFactory.java @@ -0,0 +1,52 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; + +import java.util.List; + + +/** + * Component for creating block makers. + * @author dr6 + */ +@Component +public class BlockMakerFactory { + private final TissueRepo tissueRepo; + private final SampleRepo sampleRepo; + private final SlotRepo slotRepo; + private final LabwareRepo lwRepo; + private final OperationCommentRepo opcomRepo; + private final LabwareService lwService; + private final OperationService opService; + private final WorkService workService; + private final BioRiskService bioRiskService; + + @Autowired + public BlockMakerFactory(TissueRepo tissueRepo, SampleRepo sampleRepo, SlotRepo slotRepo, LabwareRepo lwRepo, + OperationCommentRepo opcomRepo, LabwareService lwService, OperationService opService, + WorkService workService, BioRiskService bioRiskService) { + this.tissueRepo = tissueRepo; + this.sampleRepo = sampleRepo; + this.slotRepo = slotRepo; + this.lwRepo = lwRepo; + this.opcomRepo = opcomRepo; + this.lwService = lwService; + this.opService = opService; + this.workService = workService; + this.bioRiskService = bioRiskService; + } + + /** Creates a block maker for the given request with the given data. */ + public BlockMaker createBlockMaker(TissueBlockRequest request, List lwData, + Medium medium, BioState bioState, Work work, OperationType opType, User user) { + return new BlockMakerImp(tissueRepo, sampleRepo, slotRepo, lwRepo, opcomRepo, + lwService, opService, workService, bioRiskService, + request, lwData, medium, bioState, work, opType, user); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMakerImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMakerImp.java new file mode 100644 index 000000000..c5ae18b39 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockMakerImp.java @@ -0,0 +1,218 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.OperationResult; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; + +import java.util.*; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; +import static uk.ac.sanger.sccp.utils.BasicUtils.iter; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty; + +/** + * @author dr6 + */ +public class BlockMakerImp implements BlockMaker { + private final TissueRepo tissueRepo; + private final SampleRepo sampleRepo; + private final SlotRepo slotRepo; + private final LabwareRepo lwRepo; + private final OperationCommentRepo opcomRepo; + private final LabwareService lwService; + private final OperationService opService; + private final WorkService workService; + private final BioRiskService bioRiskService; + + private final TissueBlockRequest request; + private final List lwData; + private final Medium medium; + private final BioState bioState; + private final Work work; + private final OperationType opType; + private final User user; + + public BlockMakerImp(TissueRepo tissueRepo, SampleRepo sampleRepo, SlotRepo slotRepo, LabwareRepo lwRepo, + OperationCommentRepo opcomRepo, + LabwareService lwService, OperationService opService, WorkService workService, + BioRiskService bioRiskService, + TissueBlockRequest request, List lwData, + Medium medium, BioState bioState, Work work, OperationType opType, User user) { + this.tissueRepo = tissueRepo; + this.sampleRepo = sampleRepo; + this.slotRepo = slotRepo; + this.lwRepo = lwRepo; + this.opcomRepo = opcomRepo; + this.lwService = lwService; + this.opService = opService; + this.workService = workService; + this.bioRiskService = bioRiskService; + this.request = request; + this.lwData = lwData; + this.medium = medium; + this.bioState = bioState; + this.work = work; + this.opType = opType; + this.user = user; + } + + @Override + public OperationResult record() { + List labware = createLabware(); + createSamples(); + findSlots(); + fillLabware(); + List ops = createOperations(); + if (work != null) { + workService.link(work, ops); + } + bioRiskService.copyOpSampleBioRisks(ops); + discardSources(); + return new OperationResult(ops, labware); + } + + /** Create the destination labware, initially empty */ + public List createLabware() { + List labware = new ArrayList<>(lwData.size()); + for (BlockLabwareData lwd : lwData) { + String prebarcode = lwd.getRequestLabware().getPreBarcode(); + // Uses the prebarcode for the labware external barcode and the de facto barcode + Labware lw = lwService.create(lwd.getLwType(), prebarcode, prebarcode); + labware.add(lw); + lwd.setLabware(lw); + } + return labware; + } + + /** Either return the tissue or create a new tissue matching the given fields */ + public Tissue getOrCreateTissue(Tissue original, String replicate, Medium medium) { + boolean changeReplicate = (!nullOrEmpty(replicate) && !replicate.equalsIgnoreCase(original.getReplicate())); + boolean changeMedium = (medium!=null && !medium.getId().equals(original.getMedium().getId())); + if (!changeReplicate && !changeMedium) { + return original; + } + Tissue tissue = original.derived(); + if (changeReplicate) { + tissue.setReplicate(replicate.toLowerCase()); + } + if (changeMedium) { + tissue.setMedium(medium); + } + return tissueRepo.save(tissue); + } + + /** Creates the samples for the requested blocks */ + public void createSamples() { + for (BlockData bd : iter(blockDataStream())) { + Tissue original = bd.getSourceSample().getTissue(); + Tissue tissue = getOrCreateTissue(original, bd.getRequestContent().getReplicate(), medium); + Sample sample = sampleRepo.save(Sample.newBlock(null, tissue, bioState, 0)); + bd.setSample(sample); + } + } + + /** + * Finds the source and destination slots for each block + */ + public void findSlots() { + for (BlockLabwareData lwd : lwData) { + Labware destLabware = lwd.getLabware(); + for (BlockData bd : lwd.getBlocks()) { + Set
destAddresses = new HashSet<>(bd.getRequestContent().getAddresses()); + List destSlots = destLabware.getSlots().stream() + .filter(slot -> destAddresses.contains(slot.getAddress())) + .toList(); + bd.setDestSlots(destSlots); + Integer sampleId = bd.getSourceSample().getId(); + List sourceSlots = bd.getSourceLabware().getSlots().stream() + .filter(slot -> slot.getSamples().stream().anyMatch(sam -> sam.getId().equals(sampleId))) + .toList(); + bd.setSourceSlots(sourceSlots); + } + } + } + + /** Creates the required operations and records the requested comments */ + public List createOperations() { + List ops = new ArrayList<>(lwData.size()); + for (BlockLabwareData lwd : lwData) { + Operation op = createOperation(lwd); + recordComments(lwd, op.getId()); + ops.add(op); + } + return ops; + } + + /** + * Creates an operation for the specified destination labware + * with an action for each combination of source/destination slot + */ + public Operation createOperation(BlockLabwareData lwd) { + List actions = new ArrayList<>(); + for (BlockData bd : lwd.getBlocks()) { + Sample sample = bd.getSample(); + Sample sourceSample = bd.getSourceSample(); + for (Slot destSlot : bd.getDestSlots()) { + for (Slot sourceSlot : bd.getSourceSlots()) { + actions.add(new Action(null, null, sourceSlot, destSlot, sample, sourceSample)); + } + } + } + return opService.createOperation(opType, user, actions, null); + } + + /** Records the requested comments against the specified operation */ + public void recordComments(BlockLabwareData lwd, Integer opId) { + List opcoms = new ArrayList<>(); + for (BlockData bd : lwd.getBlocks()) { + Comment com = bd.getComment(); + if (com == null) { + continue; + } + Integer sampleId = bd.getSample().getId(); + for (Slot destSlot : bd.getDestSlots()) { + OperationComment opcom = new OperationComment(null, com, opId, sampleId, destSlot.getId(), null); + opcoms.add(opcom); + } + } + if (!opcoms.isEmpty()) { + opcomRepo.saveAll(opcoms); + } + } + + /** Puts the new samples into the slots and saves the updated slots */ + public void fillLabware() { + Set slotsToUpdate = new HashSet<>(); + for (BlockData bd : iter(blockDataStream())) { + for (Slot slot : bd.getDestSlots()) { + slot.addSample(bd.getSample()); + slotsToUpdate.add(slot); + } + } + slotRepo.saveAll(slotsToUpdate); + } + + /** Marks any sources discarded if the request specifies that we do so */ + public void discardSources() { + if (!nullOrEmpty(request.getDiscardSourceBarcodes())) { + Set discardBarcodes = request.getDiscardSourceBarcodes().stream() + .map(String::toUpperCase) + .collect(toSet()); + Set lwToDiscard = blockDataStream().map(BlockData::getSourceLabware) + .filter(lw -> !lw.isDiscarded() && discardBarcodes.contains(lw.getBarcode().toUpperCase())) + .collect(toSet()); + lwToDiscard.forEach(lw -> lw.setDiscarded(true)); + lwRepo.saveAll(lwToDiscard); + } + } + + /** Stream of block data from the request */ + Stream blockDataStream() { + return lwData.stream() + .flatMap(lw -> lw.getBlocks().stream()); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidator.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidator.java new file mode 100644 index 000000000..09a2daa84 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidator.java @@ -0,0 +1,41 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.service.ValidationException; + +import java.util.Collection; +import java.util.List; + +/** Utility for loading data and validating a {@link TissueBlockRequest}. */ +public interface BlockValidator { + /** + * Validates the request. + * Loads the data for the request into various fields. + */ + void validate(); + + /** Gets the collated data for the request. */ + List getLwData(); + + /** Gets the work (if any) indicated in the request. */ + Work getWork(); + + /** Gets the appropriate bio state for the request. */ + BioState getBioState(); + + /** Gets the appropriate medium for the request. */ + Medium getMedium(); + + /** Gets the appropriate operation type for the request. */ + OperationType getOpType(); + + /** Gets any problems found. */ + Collection getProblems(); + + /** + * Throws a ValidationException if there are any problems. + * @exception ValidationException if there are any problems + */ + void raiseError(); +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidatorFactory.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidatorFactory.java new file mode 100644 index 000000000..76cd5b8e1 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidatorFactory.java @@ -0,0 +1,56 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; + +/** + * Component for creating block request validators + * @author dr6 + */ +@Component +public class BlockValidatorFactory { + private final LabwareValidatorFactory lwValFactory; + private final Validator prebarcodeValidator; + private final Validator replicateValidator; + + private final LabwareRepo lwRepo; + private final OperationTypeRepo opTypeRepo; + private final LabwareTypeRepo ltRepo; + private final BioStateRepo bsRepo; + private final TissueRepo tissueRepo; + private final MediumRepo mediumRepo; + private final CommentValidationService commentValidationService; + private final WorkService workService; + + @Autowired + public BlockValidatorFactory(LabwareValidatorFactory lwValFactory, + @Qualifier("tubePrebarcodeValidator") Validator prebarcodeValidator, + @Qualifier("replicateValidator") Validator replicateValidator, + LabwareRepo lwRepo, OperationTypeRepo opTypeRepo, + LabwareTypeRepo ltRepo, BioStateRepo bsRepo, TissueRepo tissueRepo, MediumRepo mediumRepo, + CommentValidationService commentValidationService, WorkService workService) { + this.lwValFactory = lwValFactory; + this.prebarcodeValidator = prebarcodeValidator; + this.replicateValidator = replicateValidator; + this.lwRepo = lwRepo; + this.opTypeRepo = opTypeRepo; + this.ltRepo = ltRepo; + this.bsRepo = bsRepo; + this.tissueRepo = tissueRepo; + this.mediumRepo = mediumRepo; + this.commentValidationService = commentValidationService; + this.workService = workService; + } + + /** Creates a validator for the given block request. */ + public BlockValidator createBlockValidator(TissueBlockRequest request) { + return new BlockValidatorImp(lwValFactory, prebarcodeValidator, replicateValidator, + lwRepo, opTypeRepo, ltRepo, bsRepo, tissueRepo, mediumRepo, + commentValidationService, workService, request); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidatorImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidatorImp.java new file mode 100644 index 000000000..f70ecfb1f --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/block/BlockValidatorImp.java @@ -0,0 +1,480 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import org.jetbrains.annotations.NotNull; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockContent; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockLabware; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.UCMap; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; +import static uk.ac.sanger.sccp.utils.BasicUtils.*; + +/** + * Helper to validate a block request. + * @author dr6 + */ +public class BlockValidatorImp implements BlockValidator { + private final LabwareValidatorFactory lwValFactory; + private final Validator prebarcodeValidator; + private final Validator replicateValidator; + private final LabwareRepo lwRepo; + private final OperationTypeRepo opTypeRepo; + private final LabwareTypeRepo ltRepo; + private final BioStateRepo bsRepo; + private final TissueRepo tissueRepo; + private final MediumRepo mediumRepo; + private final CommentValidationService commentValidationService; + private final WorkService workService; + + private final TissueBlockRequest request; + private List lwData; + private Work work; + private BioState bioState; + private Medium medium; + private OperationType opType; + + private Collection problems; + + public BlockValidatorImp(LabwareValidatorFactory lwValFactory, + Validator prebarcodeValidator, Validator replicateValidator, + LabwareRepo lwRepo, OperationTypeRepo opTypeRepo, LabwareTypeRepo ltRepo, + BioStateRepo bsRepo, TissueRepo tissueRepo, MediumRepo mediumRepo, + CommentValidationService commentValidationService, WorkService workService, + TissueBlockRequest request) { + this.lwValFactory = lwValFactory; + this.prebarcodeValidator = prebarcodeValidator; + this.replicateValidator = replicateValidator; + this.lwRepo = lwRepo; + this.opTypeRepo = opTypeRepo; + this.ltRepo = ltRepo; + this.bsRepo = bsRepo; + this.tissueRepo = tissueRepo; + this.mediumRepo = mediumRepo; + this.commentValidationService = commentValidationService; + this.workService = workService; + this.request = request; + } + + /** Loads an entity from the database or returns null and records a problem if it is not found. */ + E loadFromOpt(String typeName, Function> loader, String name) { + Optional opt = loader.apply(name); + if (opt.isPresent()) { + return opt.get(); + } + problems.add(String.format("%s %s not found in database.", typeName, repr(name))); + return null; + } + + @Override + public void validate() { + setProblems(new LinkedHashSet<>()); + if (nullOrEmpty(request.getLabware())) { + problems.add("No labware specified."); + return; + } + setLwData(request.getLabware().stream().map(BlockLabwareData::new).toList()); + loadEntities(); + checkDestAddresses(); + checkPrebarcodes(); + checkReplicates(); + checkDiscardBarcodes(); + } + + /** + * Loads the entities referenced by the request. + */ + public void loadEntities() { + setWork(workService.validateUsableWork(problems, request.getWorkNumber())); + setBioState(loadFromOpt("Bio state", bsRepo::findByName, "Original sample")); + setMedium(loadFromOpt("Medium", mediumRepo::findByName, "OCT")); + setOpType(loadFromOpt("Operation type", opTypeRepo::findByName, "Block processing")); + loadSources(); + loadSourceSamples(); + loadLabwareTypes(); + loadComments(); + } + + /** + * Loads the source labware into a map. + */ + public void loadSources() { + Set barcodes = new HashSet<>(); + boolean anyMissing = false; + for (var block : iter(requestContents())) { + String barcode = block.getSourceBarcode(); + if (nullOrEmpty(barcode)) { + anyMissing = true; + } else { + barcodes.add(barcode.toUpperCase()); + } + } + if (anyMissing) { + problems.add("Source barcode missing."); + } + LabwareValidator val = lwValFactory.getValidator(); + val.loadLabware(lwRepo, barcodes); + val.validateSources(); + if (bioState!=null) { + val.validateBioState(bioState); + } + problems.addAll(val.getErrors()); + UCMap sourceLabware = UCMap.from(val.getLabware(), Labware::getBarcode); + for (BlockData bd : iter(blockDatas())) { + bd.setSourceLabware(sourceLabware.get(bd.getRequestContent().getSourceBarcode())); + } + } + + /** Loads the source samples into a map. */ + public void loadSourceSamples() { + boolean anyMissing = false; + for (var lwd : lwData) { + for (var bd : lwd.getBlocks()) { + Integer sourceSampleId = bd.getRequestContent().getSourceSampleId(); + if (sourceSampleId==null) { + anyMissing = true; + continue; + } + + Labware lw = bd.getSourceLabware(); + if (lw==null) { + continue; + } + Sample sample = lw.getSlots().stream().flatMap(slot -> slot.getSamples().stream()) + .filter(sam -> sam.getId().equals(sourceSampleId)) + .findFirst() + .orElse(null); + if (sample==null) { + problems.add(String.format("Sample id %s not present in labware %s.", sourceSampleId, lw.getBarcode())); + } else { + bd.setSourceSample(sample); + } + } + } + if (anyMissing) { + problems.add("Source sample ID missing from block request."); + } + } + + /** Loads the labware types into the data objects. */ + public void loadLabwareTypes() { + Set lwTypeNames = lwData.stream() + .map(ld -> ld.getRequestLabware().getLabwareType()) + .filter(s -> !nullOrEmpty(s)) + .map(String::toUpperCase) + .collect(toSet()); + UCMap lwTypes = UCMap.from(ltRepo.findAllByNameIn(lwTypeNames), LabwareType::getName); + boolean anyMissing = false; + Set unknown = new LinkedHashSet<>(); + for (BlockLabwareData ld : lwData) { + TissueBlockLabware rlw = ld.getRequestLabware(); + if (nullOrEmpty(rlw.getLabwareType())) { + anyMissing = true; + } else { + LabwareType lt = lwTypes.get(rlw.getLabwareType()); + if (lt==null) { + unknown.add(repr(rlw.getLabwareType())); + } else { + ld.setLwType(lt); + } + } + } + if (anyMissing) { + problems.add("Missing labware type."); + } + if (!unknown.isEmpty()) { + problems.add("Unknown labware types: "+unknown); + } + } + + /** Loads the referenced comment into the data objects. */ + public void loadComments() { + Stream commentIdStream = requestContents() + .map(TissueBlockContent::getCommentId) + .filter(Objects::nonNull); + Map commentMap = commentValidationService.validateCommentIds(problems, commentIdStream).stream() + .collect(inMap(Comment::getId)); + for (BlockData bd : iter(lwData.stream().flatMap(ld -> ld.getBlocks().stream()))) { + bd.setComment(commentMap.get(bd.getRequestContent().getCommentId())); + } + } + + /** Checks that the destination addresses are valid. */ + public void checkDestAddresses() { + boolean anyMissing = false; + for (var lwd : lwData) { + LabwareType lt = lwd.getLwType(); + for (var bl : lwd.getBlocks()) { + List
addresses = bl.getRequestContent().getAddresses(); + if (nullOrEmpty(addresses)) { + anyMissing = true; + } else { + for (Address addr : addresses) { + if (lt != null && lt.indexOf(addr) < 0) { + problems.add(String.format("Slot address %s not valid in labware type %s.", addr, lt.getName())); + } + } + } + } + } + if (anyMissing) { + problems.add("Destination slot addresses missing from block request."); + } + } + + /** + * Checks that the prebarcodes look appropriate. + * Prebarcodes should be given for labware types that require it; and not given for labware types + * that do not require it. + * Prebarcodes should be of the expected format. + * Prebarcodes should be unique. + */ + public void checkPrebarcodes() { + Set missingBarcodeForLwTypes = new LinkedHashSet<>(); + Set unexpectedBarcodeForLwTypes = new LinkedHashSet<>(); + Set prebarcodes = new HashSet<>(); + Set dupes = new LinkedHashSet<>(); + for (var lwd : lwData) { + LabwareType lt = lwd.getLwType(); + String barcode = lwd.getRequestLabware().getPreBarcode(); + if (nullOrEmpty(barcode)) { + if (lt != null && lt.isPrebarcoded()) { + missingBarcodeForLwTypes.add(lt.getName()); + } + } else { + prebarcodeValidator.validate(barcode, problems::add); + if (lt != null && !lt.isPrebarcoded()) { + unexpectedBarcodeForLwTypes.add(lt.getName()); + } + barcode = barcode.toUpperCase(); + if (!prebarcodes.add(barcode)) { + dupes.add(barcode); + } + } + } + + if (!missingBarcodeForLwTypes.isEmpty()) { + problems.add(pluralise("A barcode is required for labware type{s} ", missingBarcodeForLwTypes.size()) + + missingBarcodeForLwTypes + "."); + } + if (!unexpectedBarcodeForLwTypes.isEmpty()) { + problems.add(pluralise("A barcode is not expected for labware type{s} ", unexpectedBarcodeForLwTypes.size()) + + unexpectedBarcodeForLwTypes + "."); + } + if (!dupes.isEmpty()) { + problems.add("Barcode specified multiple times: "+dupes); + } + + if (!prebarcodes.isEmpty()) { + Collection existing = lwRepo.findByBarcodeIn(prebarcodes); + if (!existing.isEmpty()) { + List existingBarcodes = existing.stream() + .map(Labware::getBarcode) + .toList(); + problems.add("Barcode already in use: "+existingBarcodes); + } else { + existing = lwRepo.findByExternalBarcodeIn(prebarcodes); + if (!existing.isEmpty()) { + List existingBarcodes = existing.stream() + .map(Labware::getExternalBarcode) + .toList(); + problems.add("External barcode already in use: " + existingBarcodes); + } + } + } + + } + + /** + * Checks the formatting of the replicate field, if provided; otherwise, assigns the same replicate as the source. + */ + public void checkReplicates() { + boolean anyMissing = false; + boolean differentRep = false; + Set repKeys = new LinkedHashSet<>(); + Set repKeyDupes = new LinkedHashSet<>(); + for (var bl : iter(blockDatas())) { + String newRep = bl.getRequestContent().getReplicate(); + Sample sourceSample = bl.getSourceSample(); + String sourceRep = Optional.ofNullable(sourceSample) + .map(Sample::getTissue) + .map(Tissue::getReplicate) + .orElse(null); + if (nullOrEmpty(sourceRep)) { + if (nullOrEmpty(newRep)) { + anyMissing = true; + } else if (replicateValidator.validate(newRep, problems::add)) { + RepKey repKey = RepKey.from(sourceSample, bl.getRequestContent().getReplicate()); + if (repKey != null && !repKeys.add(repKey)) { + repKeyDupes.add(repKey); + } + } + } else if (nullOrEmpty(newRep)) { + bl.getRequestContent().setReplicate(sourceRep); + } else if (!newRep.equalsIgnoreCase(sourceRep)) { + differentRep = true; + } + } + + if (!repKeyDupes.isEmpty()) { + problems.add("Same replicate specified multiple times: " + repKeyDupes); + } + if (anyMissing) { + problems.add("Missing replicate for some blocks."); + } + if (differentRep) { + problems.add("Replicate numbers must match the source replicate number where present."); + } + List alreadyExistRepKeys = repKeys.stream() + .filter(rp -> !tissueRepo.findByDonorIdAndSpatialLocationIdAndReplicate( + rp.donorId(), rp.spatialLocationId(), rp.replicate()).isEmpty() + ).toList(); + if (!alreadyExistRepKeys.isEmpty()) { + problems.add("Replicate already exists in the database: " + alreadyExistRepKeys); + } + } + + /** Checks that the discard barcodes are source barcodes in the request. */ + public void checkDiscardBarcodes() { + List discardBarcodes = request.getDiscardSourceBarcodes(); + if (discardBarcodes.isEmpty()) { + return; + } + Set sourceBarcodes = requestContents() + .map(TissueBlockContent::getSourceBarcode) + .filter(s -> !nullOrEmpty(s)) + .map(String::toUpperCase) + .collect(toSet()); + boolean anyNull = false; + Set missingBarcodes = new LinkedHashSet<>(); + for (String barcode : discardBarcodes) { + if (nullOrEmpty(barcode)) { + anyNull = true; + } else if (!sourceBarcodes.contains(barcode.toUpperCase())) { + missingBarcodes.add(repr(barcode)); + } + } + if (anyNull) { + problems.add("A null or empty string was supplied as a barcode to discard."); + } + if (!missingBarcodes.isEmpty()) { + problems.add(pluralise("The given list of barcodes to discard includes {a |}barcode{s} " + + "that {is|are} not specified as {a |}source barcode{s} in this request: ", missingBarcodes.size()) + + missingBarcodes); + } + } + + public void setLwData(List lwData) { + this.lwData = lwData; + } + + @Override + public List getLwData() { + return this.lwData; + } + + public void setWork(Work work) { + this.work = work; + } + + @Override + public Work getWork() { + return this.work; + } + + public void setBioState(BioState bioState) { + this.bioState = bioState; + } + + @Override + public BioState getBioState() { + return this.bioState; + } + + public void setMedium(Medium medium) { + this.medium = medium; + } + + @Override + public Medium getMedium() { + return this.medium; + } + + public void setOpType(OperationType opType) { + this.opType = opType; + } + + @Override + public OperationType getOpType() { + return this.opType; + } + + public void setProblems(Collection problems) { + this.problems = problems; + } + + @Override + public Collection getProblems() { + return this.problems; + } + + @Override + public void raiseError() { + if (!problems.isEmpty()) { + throw new ValidationException(problems); + } + } + + /** A stream of the contents in the request. */ + Stream requestContents() { + return request.getLabware().stream().flatMap(rlw -> rlw.getContents().stream()); + } + + /** A stream of the block data for the request */ + Stream blockDatas() { + return lwData.stream().flatMap(lwd -> lwd.getBlocks().stream()); + } + + + /** The unique fields associated with a block */ + record RepKey(Donor donor, SpatialLocation spatialLocation, String replicate) { + RepKey(Donor donor, SpatialLocation spatialLocation, String replicate) { + this.donor = donor; + this.spatialLocation = spatialLocation; + this.replicate = replicate.toLowerCase(); + } + + Integer donorId() { + return this.donor.getId(); + } + Integer spatialLocationId() { + return this.spatialLocation.getId(); + } + + static RepKey from(Sample sample, String replicate) { + if (sample==null) { + return null; + } + Tissue tissue = sample.getTissue(); + if (tissue==null) { + return null; + } + return new RepKey(tissue.getDonor(), tissue.getSpatialLocation(), replicate); + } + + @NotNull + @Override + public String toString() { + return String.format("{Donor: %s, Tissue type: %s, Spatial location: %s, Replicate: %s}", + donor.getDonorName(), spatialLocation.getTissueType().getName(), spatialLocation.getCode(), + replicate); + } + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java index 779f7f50e..9e7fe5ea5 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java @@ -367,28 +367,28 @@ public Map getPlanActionMap(Collection planActi * @param ops the operations creating the new sections */ public void updateSourceBlocks(Collection ops) { - Map slotsToUpdate = new HashMap<>(); + Map samplesToUpdate = new HashMap<>(); for (Operation op : ops) { for (Action action : op.getActions()) { - Slot src = action.getSource(); + Sample srcSample = action.getSourceSample(); Sample sample = action.getSample(); - if (src.isBlock() && sample.getSection() != null) { - Integer alreadyHighestSection = src.getBlockHighestSection(); - Slot slotInMap = slotsToUpdate.get(src.getId()); - if (slotInMap!=null) { - if (alreadyHighestSection==null || slotInMap.getBlockHighestSection() > alreadyHighestSection) { - alreadyHighestSection = slotInMap.getBlockHighestSection(); - } - } + if (srcSample.isBlock() && sample.getSection() != null) { Integer sectionInt = parseSectionInt(sample.getSection()); - if (sectionInt != null && (alreadyHighestSection == null || alreadyHighestSection < sectionInt)) { - src.setBlockHighestSection(sectionInt); - slotsToUpdate.put(src.getId(), src); + if (sectionInt != null) { + Sample sampleInMap = samplesToUpdate.get(srcSample.getId()); + if (sampleInMap != null) { + if (sampleInMap.getBlockHighestSection() < sectionInt) { + sampleInMap.setBlockHighestSection(sectionInt); + } + } else if (srcSample.getBlockHighestSection() < sectionInt) { + srcSample.setBlockHighestSection(sectionInt); + samplesToUpdate.put(srcSample.getId(), srcSample); + } } } } } - slotRepo.saveAll(slotsToUpdate.values()); + sampleRepo.saveAll(samplesToUpdate.values()); } /** diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionValidationServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionValidationServiceImp.java index 0daf9cffc..14ebd774e 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionValidationServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionValidationServiceImp.java @@ -290,7 +290,7 @@ public void validateSections(Collection problems, List c if (dest.getLabwareId().equals(lw.getId())) { final Integer sampleId = pa.getSample().getId(); plannedSampleIds.computeIfAbsent(dest.getAddress(), ad -> new HashSet<>()).add(sampleId); - Integer lastSection = pa.getSource().getBlockHighestSection(); + Integer lastSection = pa.getSample().getBlockHighestSection(); if (lastSection != null) { Integer max = sampleMaxSection.get(sampleId); if (max == null || max < lastSection) { diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockFieldChecker.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockFieldChecker.java new file mode 100644 index 000000000..1c99aab92 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockFieldChecker.java @@ -0,0 +1,107 @@ +package uk.ac.sanger.sccp.stan.service.register; + +import org.springframework.stereotype.Service; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.request.register.*; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Utility for checking that the description of tissue in a {@link BlockRegisterRequest} + * matches the information in an existing tissue. + * @author dr6 + */ +@Service +public class BlockFieldChecker { + /** Chains two functions, but skips the second if the first returns null */ + private static Function chain(Function ab, Function bc) { + return ab.andThen(b -> b==null ? null : bc.apply(b)); + } + enum Field { + DONOR(null, BlockRegisterSample::getDonorIdentifier, Tissue::getDonor, Donor::getDonorName, "donor identifier"), + HMDMC(null, BlockRegisterSample::getHmdmc, Tissue::getHmdmc, Hmdmc::getHmdmc, "HuMFre number"), + TTYPE(null, BlockRegisterSample::getTissueType, Tissue::getTissueType, TissueType::getName, "tissue type"), + SL(null, BlockRegisterSample::getSpatialLocation, Tissue::getSpatialLocation, SpatialLocation::getCode, "spatial location"), + REPLICATE(null, BlockRegisterSample::getReplicateNumber, Tissue::getReplicate, null, "replicate number"), + MEDIUM(BlockRegisterLabware::getMedium, null, Tissue::getMedium, Medium::getName, "medium"), + FIXATIVE(BlockRegisterLabware::getFixative, null, Tissue::getFixative, Fixative::getName, "fixative"), + COLLECTION_DATE(null, BlockRegisterSample::getSampleCollectionDate, Tissue::getCollectionDate, null, "sample collection date", true), + CELL_CLASS(null, BlockRegisterSample::getCellClass, Tissue::getCellClass, CellClass::getName, "cellular classification"), + ; + + private final Function tissueFunction; + private final Function brsFunction; + private final Function brlFunction; + private final String description; + private final boolean replaceMissing; + + Field(Function brlFunction, Function brsFunction, Function tissueFunction, Function subFunction, String description, boolean replaceMissing) { + this.brlFunction = brlFunction; + this.brsFunction = brsFunction; + this.tissueFunction = subFunction==null ? tissueFunction : chain(tissueFunction, subFunction); + this.description = description; + this.replaceMissing = replaceMissing; + } + + Field(Function brlFunction, Function brsFunction, Function tissueFunction, Function subFunction, String description) { + this(brlFunction, brsFunction, tissueFunction, subFunction, description, false); + } + + /** + * Gets the value of the field from the given request parts. + * @param brl the labware part of the request + * @param brs the sample part of the request + * @return the value of the field + */ + public Object apply(BlockRegisterLabware brl, BlockRegisterSample brs) { + return brlFunction!=null ? brlFunction.apply(brl) : brsFunction.apply(brs); + } + + /** + * Gets the value of the field from the given tissue. + * @param tissue existing tissue + * @return the value of the field + */ + public Object apply(Tissue tissue) { + return tissueFunction.apply(tissue); + } + } + + /** + * Checks for discrepancies between the existing tissue and the values in the request + * @param problemConsumer receptacle for problems found + * @param brl labware part of the request + * @param brs sample part of the request + * @param tissue existing tissue + */ + public void check(Consumer problemConsumer, BlockRegisterLabware brl, BlockRegisterSample brs, Tissue tissue) { + for (Field field : Field.values()) { + Object oldValue = field.apply(tissue); + if (field.replaceMissing && oldValue==null) { + continue; + } + Object newValue = field.apply(brl, brs); + if (!match(oldValue, newValue)) { + problemConsumer.accept(String.format("Expected %s to be %s for existing tissue %s.", + field.description, oldValue, tissue.getExternalName())); + } + } + } + + /** + * Checks whether two values match, ignoring string capitalisation, and ignoring an empty new value if the old value is null + * @param oldValue old value + * @param newValue new value + * @return true if the values match; false otherwise + */ + public static boolean match(Object oldValue, Object newValue) { + if (oldValue==null) { + return (newValue==null || newValue.equals("")); + } + if (oldValue instanceof String && newValue instanceof String) { + return ((String) oldValue).equalsIgnoreCase((String) newValue); + } + return oldValue.equals(newValue); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockRegisterServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockRegisterServiceImp.java new file mode 100644 index 000000000..e5e4df90c --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockRegisterServiceImp.java @@ -0,0 +1,220 @@ +package uk.ac.sanger.sccp.stan.service.register; + +import org.springframework.stereotype.Service; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.register.*; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.UCMap; + +import javax.persistence.EntityManager; +import java.util.*; +import java.util.stream.Stream; + +import static uk.ac.sanger.sccp.utils.BasicUtils.iter; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty; + +/** + * Register service for {@link BlockRegisterRequest} + * @author dr6 + */ +@Service +public class BlockRegisterServiceImp implements IRegisterService { + private final EntityManager entityManager; + private final RegisterValidationFactory validationFactory; + private final DonorRepo donorRepo; + private final TissueRepo tissueRepo; + private final SampleRepo sampleRepo; + private final SlotRepo slotRepo; + private final BioRiskRepo bioRiskRepo; + private final OperationTypeRepo opTypeRepo; + private final LabwareService labwareService; + private final OperationService operationService; + private final WorkService workService; + private final RegisterClashChecker clashChecker; + + public BlockRegisterServiceImp(EntityManager entityManager, + RegisterValidationFactory validationFactory, + DonorRepo donorRepo, TissueRepo tissueRepo, SampleRepo sampleRepo, + SlotRepo slotRepo, BioRiskRepo bioRiskRepo, OperationTypeRepo opTypeRepo, + LabwareService labwareService, OperationService operationService, + WorkService workService, + RegisterClashChecker clashChecker) { + this.entityManager = entityManager; + this.validationFactory = validationFactory; + this.donorRepo = donorRepo; + this.tissueRepo = tissueRepo; + this.sampleRepo = sampleRepo; + this.slotRepo = slotRepo; + this.bioRiskRepo = bioRiskRepo; + this.opTypeRepo = opTypeRepo; + this.labwareService = labwareService; + this.operationService = operationService; + this.workService = workService; + this.clashChecker = clashChecker; + } + + @Override + public RegisterResult register(User user, BlockRegisterRequest request) throws ValidationException { + if (request.getLabware().isEmpty()) { + return new RegisterResult(); // nothing to do + } + List clashes = clashChecker.findClashes(request); + if (!clashes.isEmpty()) { + return RegisterResult.clashes(clashes); + } + RegisterValidation validation = validationFactory.createBlockRegisterValidation(request); + Collection problems = validation.validate(); + if (!problems.isEmpty()) { + throw new ValidationException("The register request could not be validated.", problems); + } + updateExistingTissues(request, validation); + return create(request, user, validation); + } + + /** + * Updates any existing tissues that now have a collection date + * @param request specification + * @param validation validation result to look up tissues + */ + public void updateExistingTissues(BlockRegisterRequest request, RegisterValidation validation) { + List toUpdate = new ArrayList<>(); + for (BlockRegisterSample brs : iter(requestSamples(request))) { + if (brs.isExistingTissue() && brs.getSampleCollectionDate()!=null) { + Tissue tissue = validation.getTissue(brs.getExternalIdentifier()); + if (tissue!=null && tissue.getCollectionDate()==null) { + tissue.setCollectionDate(brs.getSampleCollectionDate()); + toUpdate.add(tissue); + } + } + } + if (!toUpdate.isEmpty()) { + tissueRepo.saveAll(toUpdate); + } + } + + /** + * Creates donors that are already in the database + * @return map of donor name to donor + */ + public UCMap createDonors(BlockRegisterRequest request, RegisterValidation validation) { + UCMap donors = new UCMap<>(); + for (BlockRegisterSample brs : iter(requestSamples(request))) { + String donorName = brs.getDonorIdentifier(); + if (!donors.containsKey(donorName)) { + Donor donor = validation.getDonor(donorName); + if (donor.getId() == null) { + donor = donorRepo.save(donor); + } + donors.put(donorName, donor); + } + } + return donors; + } + + /** + * Creates tissues that aren't already in the database + * @return map of external identifier to tissue + */ + public UCMap createTissues(BlockRegisterRequest request, RegisterValidation validation) { + UCMap donors = createDonors(request, validation); + UCMap tissueMap = new UCMap<>(); + for (BlockRegisterLabware brl : request.getLabware()) { + for (BlockRegisterSample brs : brl.getSamples()) { + final String tissueKey = brs.getExternalIdentifier(); + if (tissueMap.get(tissueKey) != null) { + continue; + } + Tissue existingTissue = validation.getTissue(tissueKey); + if (existingTissue != null) { + tissueMap.put(tissueKey, existingTissue); + continue; + } + Donor donor = donors.get(brs.getDonorIdentifier()); + Hmdmc hmdmc; + if (nullOrEmpty(brs.getHmdmc())) { + hmdmc = null; + } else { + hmdmc = validation.getHmdmc(brs.getHmdmc()); + if (hmdmc == null) { + throw new IllegalArgumentException("Unknown HuMFre number: " + brs.getHmdmc()); + } + } + CellClass cellClass = validation.getCellClass(brs.getCellClass()); + if (hmdmc == null && donor.getSpecies().requiresHmdmc() && cellClass.isHmdmcRequired()) { + throw new IllegalArgumentException("No HuMFre number given for tissue " + brs.getExternalIdentifier()); + } + if (!donor.getSpecies().requiresHmdmc() && hmdmc != null) { + throw new IllegalArgumentException("HuMFre number given for non-human tissue " + brs.getExternalIdentifier()); + } + Tissue tissue = new Tissue(null, brs.getExternalIdentifier(), brs.getReplicateNumber().toLowerCase(), + validation.getSpatialLocation(brs.getTissueType(), brs.getSpatialLocation()), + donor, + validation.getMedium(brl.getMedium()), + validation.getFixative(brl.getFixative()), cellClass, + hmdmc, brs.getSampleCollectionDate(), null); + tissueMap.put(tissueKey, tissueRepo.save(tissue)); + } + } + return tissueMap; + } + + /** + * Creates the labware and operations for the given registration request + * @param request the registration request + * @param user the user responsible for the operations + * @param validation the data from validation + * @return result containing the new labware + */ + public RegisterResult create(BlockRegisterRequest request, User user, RegisterValidation validation) { + UCMap tissues = createTissues(request, validation); + List lwList = new ArrayList<>(); + List opList = new ArrayList<>(); + OperationType opType = opTypeRepo.getByName("Register"); + BioState bioState = opType.getNewBioState(); + for (BlockRegisterLabware brl : request.getLabware()) { + LabwareType labwareType = validation.getLabwareType(brl.getLabwareType()); + String xb = brl.getExternalBarcode(); + Labware lw = labwareService.create(labwareType, xb, xb); + lwList.add(lw); + Set slotsToUpdate = new HashSet<>(); + List actions = new ArrayList<>(); + List sampleBioRisks = new ArrayList<>(); + for (BlockRegisterSample brs : brl.getSamples()) { + Tissue tissue = tissues.get(brs.getExternalIdentifier()); + Sample sample = sampleRepo.save(Sample.newBlock(null, tissue, bioState, brs.getHighestSection())); + Set
addressSet = new HashSet<>(brs.getAddresses()); + List slots = lw.getSlots().stream() + .filter(slot -> addressSet.contains(slot.getAddress())) + .toList(); + for (Slot slot : slots) { + slot.addSample(sample); + slotsToUpdate.add(slot); + actions.add(new Action(null, null, slot, slot, sample, sample)); + } + BioRisk bioRisk = validation.getBioRisk(brs.getBioRiskCode()); + sampleBioRisks.add(new SampleBioRisk(sample, bioRisk)); + } + entityManager.refresh(lw); + slotRepo.saveAll(slotsToUpdate); + Operation op = operationService.createOperation(opType, user, actions, null); + opList.add(op); + for (SampleBioRisk sampleBioRisk : sampleBioRisks) { + bioRiskRepo.recordBioRisk(sampleBioRisk.sample, sampleBioRisk.bioRisk, op.getId()); + } + } + if (!opList.isEmpty() && !nullOrEmpty(validation.getWorks())) { + workService.link(validation.getWorks(), opList); + } + + return new RegisterResult(lwList); + } + + /** Stream the samples in the request */ + Stream requestSamples(BlockRegisterRequest request) { + return request.getLabware().stream().flatMap(brl -> brl.getSamples().stream()); + } + + record SampleBioRisk(Sample sample, BioRisk bioRisk) {} +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockRegisterValidationImp.java similarity index 51% rename from src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java rename to src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockRegisterValidationImp.java index 906424ab7..4d6fe58e5 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/BlockRegisterValidationImp.java @@ -2,8 +2,7 @@ import uk.ac.sanger.sccp.stan.model.*; import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.request.register.*; import uk.ac.sanger.sccp.stan.service.BioRiskService; import uk.ac.sanger.sccp.stan.service.Validator; import uk.ac.sanger.sccp.stan.service.work.WorkService; @@ -13,15 +12,15 @@ import java.time.LocalDate; import java.util.*; import java.util.function.Function; +import java.util.stream.Stream; import static uk.ac.sanger.sccp.utils.BasicUtils.*; /** * @author dr6 */ -// This might have been a nice idea, but in practice it's unnecessarily complicated. -public class RegisterValidationImp implements RegisterValidation { - private final RegisterRequest request; +public class BlockRegisterValidationImp implements RegisterValidation { + private final BlockRegisterRequest request; private final DonorRepo donorRepo; private final HmdmcRepo hmdmcRepo; private final TissueTypeRepo ttRepo; @@ -31,34 +30,36 @@ public class RegisterValidationImp implements RegisterValidation { private final TissueRepo tissueRepo; private final SpeciesRepo speciesRepo; private final CellClassRepo cellClassRepo; - private final Validator donorNameValidation; - private final Validator externalNameValidation; + private final LabwareRepo lwRepo; + private final Validator donorNameValidator; + private final Validator externalNameValidator; private final Validator replicateValidator; - private final TissueFieldChecker tissueFieldChecker; + private final Validator externalBarcodeValidator; + private final BlockFieldChecker blockFieldChecker; private final BioRiskService bioRiskService; private final WorkService workService; - final Map donorMap = new HashMap<>(); - final Map tissueMap = new HashMap<>(); - final Map hmdmcMap = new HashMap<>(); - final Map speciesMap = new HashMap<>(); + final UCMap donorMap = new UCMap<>(); + final UCMap tissueMap = new UCMap<>(); + final UCMap hmdmcMap = new UCMap<>(); + final UCMap speciesMap = new UCMap<>(); final Map spatialLocationMap = new HashMap<>(); - final Map labwareTypeMap = new HashMap<>(); - final Map mediumMap = new HashMap<>(); - final Map fixativeMap = new HashMap<>(); + final UCMap labwareTypeMap = new UCMap<>(); + final UCMap mediumMap = new UCMap<>(); + final UCMap fixativeMap = new UCMap<>(); UCMap cellClassMap; UCMap bioRiskMap; Collection works; final LinkedHashSet problems = new LinkedHashSet<>(); - public RegisterValidationImp(RegisterRequest request, DonorRepo donorRepo, - HmdmcRepo hmdmcRepo, TissueTypeRepo ttRepo, LabwareTypeRepo ltRepo, - MediumRepo mediumRepo, FixativeRepo fixativeRepo, TissueRepo tissueRepo, - SpeciesRepo speciesRepo, CellClassRepo cellClassRepo, - Validator donorNameValidation, Validator externalNameValidation, - Validator replicateValidator, - TissueFieldChecker tissueFieldChecker, - BioRiskService bioRiskService, WorkService workService) { + public BlockRegisterValidationImp(BlockRegisterRequest request, DonorRepo donorRepo, + HmdmcRepo hmdmcRepo, TissueTypeRepo ttRepo, LabwareTypeRepo ltRepo, + MediumRepo mediumRepo, FixativeRepo fixativeRepo, TissueRepo tissueRepo, + SpeciesRepo speciesRepo, CellClassRepo cellClassRepo, LabwareRepo lwRepo, + Validator donorNameValidator, Validator externalNameValidator, + Validator replicateValidator, Validator externalBarcodeValidator, + BlockFieldChecker blockFieldChecker, + BioRiskService bioRiskService, WorkService workService) { this.request = request; this.donorRepo = donorRepo; this.hmdmcRepo = hmdmcRepo; @@ -69,23 +70,28 @@ public RegisterValidationImp(RegisterRequest request, DonorRepo donorRepo, this.tissueRepo = tissueRepo; this.speciesRepo = speciesRepo; this.cellClassRepo = cellClassRepo; - this.donorNameValidation = donorNameValidation; - this.externalNameValidation = externalNameValidation; + this.lwRepo = lwRepo; + this.donorNameValidator = donorNameValidator; + this.externalNameValidator = externalNameValidator; this.replicateValidator = replicateValidator; - this.tissueFieldChecker = tissueFieldChecker; + this.externalBarcodeValidator = externalBarcodeValidator; + this.blockFieldChecker = blockFieldChecker; this.bioRiskService = bioRiskService; this.workService = workService; } @Override public Collection validate() { - if (blocks().isEmpty()) { - return Collections.emptySet(); // nothing to do + if (request.getLabware().stream().allMatch(blw -> blw.getSamples().isEmpty())) { + problems.add("No labware specified in request."); + return problems; } validateDonors(); validateHmdmcs(); validateSpatialLocations(); validateLabwareTypes(); + validateExternalBarcodes(); + validateAddresses(); validateMediums(); validateFixatives(); validateCollectionDates(); @@ -97,26 +103,79 @@ public Collection validate() { return problems; } + @Override + public Donor getDonor(String name) { + return donorMap.get(name); + } + + @Override + public Hmdmc getHmdmc(String hmdmc) { + return hmdmcMap.get(hmdmc); + } + + @Override + public SpatialLocation getSpatialLocation(String tissueTypeName, int code) { + if (tissueTypeName==null) { + return null; + } + return spatialLocationMap.get(new StringIntKey(tissueTypeName, code)); + } + + @Override + public LabwareType getLabwareType(String name) { + return labwareTypeMap.get(name); + } + + @Override + public Medium getMedium(String name) { + return mediumMap.get(name); + } + + @Override + public Fixative getFixative(String name) { + return fixativeMap.get(name); + } + + @Override + public Tissue getTissue(String externalName) { + return tissueMap.get(externalName); + } + + @Override + public BioRisk getBioRisk(String code) { + return bioRiskMap.get(code); + } + + @Override + public CellClass getCellClass(String name) { + return cellClassMap.get(name); + } + + @Override + public Collection getWorks() { + return works; + } + + /** Checks the donor info for problems */ public void validateDonors() { - for (BlockRegisterRequest block : blocks()) { + for (BlockRegisterSample brs : iter(blockSamples())) { boolean skip = false; Species species = null; - if (block.getDonorIdentifier()==null || block.getDonorIdentifier().isEmpty()) { + if (nullOrEmpty(brs.getDonorIdentifier())) { skip = true; addProblem("Missing donor identifier."); - } else if (donorNameValidation!=null) { - donorNameValidation.validate(block.getDonorIdentifier(), this::addProblem); + } else if (donorNameValidator !=null) { + donorNameValidator.validate(brs.getDonorIdentifier(), this::addProblem); } - if (block.getSpecies()==null || block.getSpecies().isEmpty()) { + if (nullOrEmpty(brs.getSpecies())) { addProblem("Missing species."); } else { - String speciesUc = block.getSpecies().toUpperCase(); - species = speciesMap.get(speciesUc); - if (species==null && !speciesMap.containsKey(speciesUc)) { - species = speciesRepo.findByName(speciesUc).orElse(null); - speciesMap.put(speciesUc, species); + species = speciesMap.get(brs.getSpecies()); + if (species==null && !speciesMap.containsKey(brs.getSpecies())) { + species = speciesRepo.findByName(brs.getSpecies()).orElse(null); + speciesMap.put(brs.getSpecies(), species); if (species==null) { - addProblem("Unknown species: "+repr(block.getSpecies())); + addProblem("Unknown species: "+repr(brs.getSpecies())); } else if (!species.isEnabled()) { addProblem("Species is not enabled: "+species.getName()); } @@ -125,13 +184,12 @@ public void validateDonors() { if (skip) { continue; } - String donorNameUc = block.getDonorIdentifier().toUpperCase(); - Donor donor = donorMap.get(donorNameUc); + Donor donor = donorMap.get(brs.getDonorIdentifier()); if (donor==null) { - donor = new Donor(null, block.getDonorIdentifier(), block.getLifeStage(), species); - donorMap.put(donorNameUc, donor); + donor = new Donor(null, brs.getDonorIdentifier(), brs.getLifeStage(), species); + donorMap.put(brs.getDonorIdentifier(), donor); } else { - if (donor.getLifeStage()!=block.getLifeStage()) { + if (donor.getLifeStage()!= brs.getLifeStage()) { addProblem("Multiple different life stages specified for donor "+donor.getDonorName()); } if (species!=null && !species.equals(donor.getSpecies())) { @@ -156,11 +214,63 @@ public void validateDonors() { } } + /** Checks the HMDMCs for problems */ + public void validateHmdmcs() { + Set unknownHmdmcs = new LinkedHashSet<>(); + boolean unwanted = false; + boolean missing = false; + for (BlockRegisterSample brs : iter(blockSamples())) { + boolean needsHmdmc = false; + boolean needsNoHmdmc = false; + if (brs.getSpecies()!=null && !brs.getSpecies().isEmpty()) { + needsHmdmc = Species.isHumanName(brs.getSpecies()); + needsNoHmdmc = !needsHmdmc; + } + String hmdmcString = brs.getHmdmc(); + if (hmdmcString==null || hmdmcString.isEmpty()) { + if (needsHmdmc) { + missing = true; + } + continue; + } + if (needsNoHmdmc) { + unwanted = true; + continue; + } + + if (hmdmcMap.containsKey(hmdmcString)) { + continue; + } + Hmdmc hmdmc = hmdmcRepo.findByHmdmc(hmdmcString).orElse(null); + hmdmcMap.put(hmdmcString, hmdmc); + if (hmdmc==null) { + unknownHmdmcs.add(hmdmcString); + } + } + if (missing) { + addProblem("Missing HuMFre number."); + } + if (unwanted) { + addProblem("Non-human tissue should not have a HuMFre number."); + } + if (!unknownHmdmcs.isEmpty()) { + addProblem(pluralise("Unknown HuMFre number{s}: ", unknownHmdmcs.size()) + unknownHmdmcs); + } + List disabledHmdmcs = hmdmcMap.values().stream() + .filter(h -> h!=null && !h.isEnabled()) + .map(Hmdmc::getHmdmc) + .toList(); + if (!disabledHmdmcs.isEmpty()) { + addProblem(pluralise("HuMFre number{s} not enabled: ", disabledHmdmcs.size()) + disabledHmdmcs); + } + } + + /** Checks tissue types and spatial locations for problems */ public void validateSpatialLocations() { - Map tissueTypeMap = new HashMap<>(); + UCMap tissueTypeMap = new UCMap<>(); Set unknownTissueTypes = new LinkedHashSet<>(); - for (BlockRegisterRequest block : blocks()) { - if (block.getTissueType()==null || block.getTissueType().isEmpty()) { + for (BlockRegisterSample block : iter(blockSamples())) { + if (nullOrEmpty(block.getTissueType())) { addProblem("Missing tissue type."); continue; } @@ -207,90 +317,43 @@ public void validateSpatialLocations() { } } - public void validateHmdmcs() { - Set unknownHmdmcs = new LinkedHashSet<>(); - boolean unwanted = false; - boolean missing = false; - for (BlockRegisterRequest block : blocks()) { - boolean needsHmdmc = false; - boolean needsNoHmdmc = false; - if (block.getSpecies()!=null && !block.getSpecies().isEmpty()) { - needsHmdmc = Species.isHumanName(block.getSpecies()); - needsNoHmdmc = !needsHmdmc; - } - String hmdmcString = block.getHmdmc(); - if (hmdmcString==null || hmdmcString.isEmpty()) { - if (needsHmdmc) { - missing = true; - } - continue; - } - if (needsNoHmdmc) { - unwanted = true; - continue; - } - - String hmdmcUc = hmdmcString.toUpperCase(); - if (hmdmcMap.containsKey(hmdmcUc)) { - continue; - } - Hmdmc hmdmc = hmdmcRepo.findByHmdmc(hmdmcString).orElse(null); - hmdmcMap.put(hmdmcUc, hmdmc); - if (hmdmc==null) { - unknownHmdmcs.add(hmdmcString); - } - } - if (missing) { - addProblem("Missing HuMFre number."); - } - if (unwanted) { - addProblem("Non-human tissue should not have a HuMFre number."); - } - if (!unknownHmdmcs.isEmpty()) { - addProblem(pluralise("Unknown HuMFre number{s}: ", unknownHmdmcs.size()) + unknownHmdmcs); - } - List disabledHmdmcs = hmdmcMap.values().stream() - .filter(h -> h!=null && !h.isEnabled()) - .map(Hmdmc::getHmdmc) - .toList(); - if (!disabledHmdmcs.isEmpty()) { - addProblem(pluralise("HuMFre number{s} not enabled: ", disabledHmdmcs.size()) + disabledHmdmcs); - } - } - + /** Loads and checks the labware types for problems */ public void validateLabwareTypes() { - validateByName("labware type", BlockRegisterRequest::getLabwareType, ltRepo::findByName, labwareTypeMap); + validateByName("labware type", BlockRegisterLabware::getLabwareType, ltRepo::findByName, labwareTypeMap); } + /** Loads and checks the mediums for problems */ public void validateMediums() { - validateByName("medium", BlockRegisterRequest::getMedium, mediumRepo::findByName, mediumMap); + validateByName("medium", BlockRegisterLabware::getMedium, mediumRepo::findByName, mediumMap); } + /** Loads and checks the fixatives for problems */ public void validateFixatives() { - validateByName("fixative", BlockRegisterRequest::getFixative, fixativeRepo::findByName, fixativeMap); + validateByName("fixative", BlockRegisterLabware::getFixative, fixativeRepo::findByName, fixativeMap); } + /** Checks the collection dates for problems */ public void validateCollectionDates() { boolean missing = false; LocalDate today = LocalDate.now(); Set badDates = new LinkedHashSet<>(); - Map extToDate = new HashMap<>(request.getBlocks().size()); - for (BlockRegisterRequest block : blocks()) { - if (block.getSampleCollectionDate()==null) { - if (block.getLifeStage()==LifeStage.fetal && block.getSpecies()!=null - && Species.isHumanName(block.getSpecies())) { + UCMap extToDate = new UCMap<>(); + for (BlockRegisterSample brs : iter(blockSamples())) { + if (brs.getSampleCollectionDate()==null) { + if (brs.getLifeStage()==LifeStage.fetal && brs.getSpecies()!=null + && Species.isHumanName(brs.getSpecies())) { missing = true; } - } else if (block.getSampleCollectionDate().isAfter(today)) { - badDates.add(block.getSampleCollectionDate()); + } else if (brs.getSampleCollectionDate().isAfter(today)) { + badDates.add(brs.getSampleCollectionDate()); } - if (block.getExternalIdentifier()!=null && !block.getExternalIdentifier().isEmpty()) { - String key = block.getExternalIdentifier().trim().toUpperCase(); + if (brs.getExternalIdentifier()!=null && !brs.getExternalIdentifier().isEmpty()) { + String key = brs.getExternalIdentifier().trim().toUpperCase(); if (!key.isEmpty()) { - if (extToDate.containsKey(key) && !Objects.equals(extToDate.get(key), block.getSampleCollectionDate())) { + if (extToDate.containsKey(key) && !Objects.equals(extToDate.get(key), brs.getSampleCollectionDate())) { addProblem("Inconsistent collection dates specified for tissue " + key + "."); } else { - extToDate.put(key, block.getSampleCollectionDate()); + extToDate.put(key, brs.getSampleCollectionDate()); } } } @@ -303,85 +366,93 @@ public void validateCollectionDates() { } } + /** Looks for problems with the information given for existing tissues */ public void validateExistingTissues() { - List blocksForExistingTissues = blocks().stream() - .filter(BlockRegisterRequest::isExistingTissue) - .toList(); + List blocksForExistingTissues = new ArrayList<>(); + for (BlockRegisterLabware blw : request.getLabware()) { + for (BlockRegisterSample brs : blw.getSamples()) { + if (brs.isExistingTissue()) { + blocksForExistingTissues.add(new BlockRegisterLabwareAndSample(blw, brs)); + } + } + } if (blocksForExistingTissues.isEmpty()) { return; } - if (blocksForExistingTissues.stream().map(BlockRegisterRequest::getExternalIdentifier).anyMatch(xn -> xn==null || xn.isEmpty())) { + if (blocksForExistingTissues.stream().anyMatch(brs -> nullOrEmpty(brs.sample().getExternalIdentifier()))) { addProblem("Missing external identifier."); } Set xns = blocksForExistingTissues.stream() - .map(BlockRegisterRequest::getExternalIdentifier) - .filter(Objects::nonNull) + .map(b -> b.sample().getExternalIdentifier()) + .filter(xn -> !nullOrEmpty(xn)) .collect(toLinkedHashSet()); if (xns.isEmpty()) { return; } - tissueRepo.findAllByExternalNameIn(xns).forEach(t -> tissueMap.put(t.getExternalName().toUpperCase(), t)); + tissueRepo.findAllByExternalNameIn(xns).forEach(t -> tissueMap.put(t.getExternalName(), t)); Set missing = xns.stream() - .filter(xn -> !tissueMap.containsKey(xn.toUpperCase())) + .filter(xn -> !tissueMap.containsKey(xn)) .collect(toLinkedHashSet()); if (!missing.isEmpty()) { - addProblem("Existing external identifiers not recognised: " + reprCollection(missing)); + addProblem("Existing external identifier not recognised: " + reprCollection(missing)); } - for (BlockRegisterRequest br : blocksForExistingTissues) { - String xn = br.getExternalIdentifier(); - if (xn == null || xn.isEmpty()) { - continue; - } - Tissue tissue = tissueMap.get(xn.toUpperCase()); - if (tissue!=null) { - tissueFieldChecker.check(this::addProblem, br, tissue); + for (BlockRegisterLabwareAndSample b : blocksForExistingTissues) { + String xn = b.sample().getExternalIdentifier(); + if (!nullOrEmpty(xn)) { + Tissue tissue = tissueMap.get(xn); + if (tissue != null) { + blockFieldChecker.check(this::addProblem, b.labware(), b.sample(), tissue); + } } } } + /** Looks for problems with the information given for new tissues */ public void validateNewTissues() { // NB repeated new external identifier in one request is still disallowed Set externalNames = new HashSet<>(); - for (BlockRegisterRequest block : blocks()) { - if (block.isExistingTissue()) { + for (BlockRegisterSample brs : iter(blockSamples())) { + if (brs.isExistingTissue()) { continue; } - if (block.getReplicateNumber()==null || block.getReplicateNumber().isEmpty()) { + if (nullOrEmpty(brs.getReplicateNumber())) { addProblem("Missing replicate number."); } else { - replicateValidator.validate(block.getReplicateNumber(), this::addProblem); + replicateValidator.validate(brs.getReplicateNumber(), this::addProblem); } - if (block.getHighestSection() < 0) { + if (brs.getHighestSection() < 0) { addProblem("Highest section number cannot be negative."); } - if (block.getExternalIdentifier()==null || block.getExternalIdentifier().isEmpty()) { + if (nullOrEmpty(brs.getExternalIdentifier())) { addProblem("Missing external identifier."); } else { - if (externalNameValidation != null) { - externalNameValidation.validate(block.getExternalIdentifier(), this::addProblem); + if (externalNameValidator != null) { + externalNameValidator.validate(brs.getExternalIdentifier(), this::addProblem); } - if (!externalNames.add(block.getExternalIdentifier().toUpperCase())) { - addProblem("Repeated external identifier: " + block.getExternalIdentifier()); - } else if (!tissueRepo.findAllByExternalName(block.getExternalIdentifier()).isEmpty()) { + if (!externalNames.add(brs.getExternalIdentifier().toUpperCase())) { + addProblem("Repeated external identifier: " + brs.getExternalIdentifier()); + } else if (!tissueRepo.findAllByExternalName(brs.getExternalIdentifier()).isEmpty()) { addProblem(String.format("There is already tissue in the database with external identifier %s.", - block.getExternalIdentifier())); + brs.getExternalIdentifier())); } } } } + /** Loads and checks bio risks for problems */ public void validateBioRisks() { - this.bioRiskMap = bioRiskService.loadAndValidateBioRisks(problems, blocks().stream(), - BlockRegisterRequest::getBioRiskCode, BlockRegisterRequest::setBioRiskCode); + this.bioRiskMap = bioRiskService.loadAndValidateBioRisks(problems, blockSamples(), + BlockRegisterSample::getBioRiskCode, BlockRegisterSample::setBioRiskCode); } + /** Loads and checks cell classes for problems */ public void validateCellClasses() { Set cellClassNames = new HashSet<>(); boolean anyMissing = false; - for (BlockRegisterRequest block : blocks()) { - String cellClassName = block.getCellClass(); + for (BlockRegisterSample brs : iter(blockSamples())) { + String cellClassName = brs.getCellClass(); if (nullOrEmpty(cellClassName)) { anyMissing = true; } else { @@ -405,6 +476,7 @@ public void validateCellClasses() { } } + /** Loads and checks works for problems */ public void validateWorks() { if (request.getWorkNumbers().isEmpty()) { addProblem("No work number supplied."); @@ -414,31 +486,98 @@ public void validateWorks() { } } - private void validateByName(String entityName, - Function nameFunction, - Function> lkp, - Map map) { + /** Checks labware external barcodes for problems */ + public void validateExternalBarcodes() { + Set barcodes = new HashSet<>(); + boolean anyMissing = false; + for (BlockRegisterLabware brl : request.getLabware()) { + String barcode = brl.getExternalBarcode(); + if (nullOrEmpty(barcode)) { + anyMissing = true; + } else if (!barcodes.add(barcode.toUpperCase())) { + addProblem("External barcode given multiple times: " + barcode); + } + } + if (anyMissing) { + problems.add("Missing external barcode."); + } + for (String barcode : barcodes) { + externalBarcodeValidator.validate(barcode, this::addProblem); + } + Set usedBarcodes = lwRepo.findBarcodesByBarcodeIn(barcodes); + if (!usedBarcodes.isEmpty()) { + addProblem("Labware barcode already in use: " + usedBarcodes); + } + barcodes.removeAll(usedBarcodes); + Set usedExternalBarcodes = lwRepo.findExternalBarcodesIn(barcodes); + if (!usedExternalBarcodes.isEmpty()) { + addProblem("External barcode already in use: " + usedExternalBarcodes); + } + } + + /** Checks slot addresses for problems */ + public void validateAddresses() { + Map> lwTypeInvalidAddresses = new HashMap<>(); + boolean missing = false; + for (BlockRegisterLabware brl : request.getLabware()) { + LabwareType lt = labwareTypeMap.get(brl.getLabwareType()); + if (lt == null) { + continue; + } + for (BlockRegisterSample brs : brl.getSamples()) { + if (brs.getAddresses().isEmpty()) { + missing = true; + } else { + for (Address ad : brs.getAddresses()) { + if (lt.indexOf(ad) < 0) { + lwTypeInvalidAddresses.computeIfAbsent(lt, k -> new HashSet<>()).add(ad); + } + } + } + } + } + if (missing) { + problems.add("Slot addresses missing from request."); + } + if (!lwTypeInvalidAddresses.isEmpty()) { + lwTypeInvalidAddresses.forEach((lt, invalidAddresses) + -> problems.add(String.format("Invalid slot addresses for labware type %s: %s", + lt.getName(), invalidAddresses.stream().sorted().toList()))); + } + } + + /** + * Helper method to load entities into a map and check for problems + * @param type of entity to load + * @param entityName name of the type of entity + * @param nameFunction function to extract the name from the request + * @param lkp function to load the entity from the database + * @param map map to load the entity into + */ + void validateByName(String entityName, + Function nameFunction, + Function> lkp, + UCMap map) { Set unknownNames = new LinkedHashSet<>(); boolean missing = false; - for (BlockRegisterRequest block : blocks()) { - String name = nameFunction.apply(block); - if (name==null || name.isEmpty()) { + for (BlockRegisterLabware brl : request.getLabware()) { + String name = nameFunction.apply(brl); + if (nullOrEmpty(name)) { missing = true; continue; } if (unknownNames.contains(name)) { continue; } - String nameUc = name.toUpperCase(); - if (map.containsKey(nameUc)) { + if (map.containsKey(name)) { continue; } - Optional opt = lkp.apply(nameUc); + Optional opt = lkp.apply(name); if (opt.isEmpty()) { - unknownNames.add(name); + unknownNames.add(repr(name)); continue; } - map.put(nameUc, opt.get()); + map.put(name, opt.get()); } if (missing) { addProblem(String.format("Missing %s.", entityName)); @@ -448,76 +587,28 @@ private void validateByName(String entityName, } } - private Collection blocks() { - return request.getBlocks(); + Collection getProblems() { + return problems; } - private boolean addProblem(String problem) { + /** Adds a problem to the internal collection of problems found */ + boolean addProblem(String problem) { return problems.add(problem); } - public Collection getProblems() { - return this.problems; - } - - @Override - public Donor getDonor(String name) { - return ucGet(this.donorMap, name); - } - - @Override - public Hmdmc getHmdmc(String hmdmc) { - return ucGet(hmdmcMap, hmdmc); - } - - @Override - public SpatialLocation getSpatialLocation(String tissueTypeName, int code) { - return (tissueTypeName==null ? null : this.spatialLocationMap.get(new StringIntKey(tissueTypeName, code))); - } - - @Override - public LabwareType getLabwareType(String name) { - return ucGet(this.labwareTypeMap, name); - } - - @Override - public Medium getMedium(String name) { - return ucGet(this.mediumMap, name); - } - - @Override - public Fixative getFixative(String name) { - return ucGet(this.fixativeMap, name); - } - - @Override - public Tissue getTissue(String externalName) { - return ucGet(this.tissueMap, externalName); - } - - @Override - public BioRisk getBioRisk(String code) { - return bioRiskMap.get(code); - } - - @Override - public Collection getWorks() { - return this.works; - } - - @Override - public CellClass getCellClass(String name) { - return cellClassMap.get(name); - } - - private static E ucGet(Map map, String key) { - return (key==null ? null : map.get(key.toUpperCase())); + /** Helper method to stream samples in the request */ + Stream blockSamples() { + return this.request.getLabware().stream().flatMap(brl -> brl.getSamples().stream()); } + /** A non-null string and an int, used for case insensitive deduplication. The string is put into upper case. */ record StringIntKey(String string, int number) { StringIntKey(String string, int number) { this.string = string.toUpperCase(); this.number = number; } } + + /** A linked labware and sample from the request. Used when validating existing tissues. */ + record BlockRegisterLabwareAndSample(BlockRegisterLabware labware, BlockRegisterSample sample) {} } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/FileRegisterServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/FileRegisterServiceImp.java index 9dd34f9b3..ed4169101 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/FileRegisterServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/FileRegisterServiceImp.java @@ -23,19 +23,19 @@ @Service public class FileRegisterServiceImp implements FileRegisterService { private final IRegisterService sectionRegisterService; - private final IRegisterService blockRegisterService; + private final IRegisterService blockRegisterService; private final IRegisterService originalSampleRegisterService; private final MultipartFileReader sectionFileReader; - private final MultipartFileReader blockFileReader; + private final MultipartFileReader blockFileReader; private final MultipartFileReader originalSampleFileReader; private final Transactor transactor; @Autowired public FileRegisterServiceImp(IRegisterService sectionRegisterService, - IRegisterService blockRegisterService, + IRegisterService blockRegisterService, IRegisterService originalSampleRegisterService, MultipartFileReader sectionFileReader, - MultipartFileReader blockFileReader, + MultipartFileReader blockFileReader, MultipartFileReader originalSampleFileReader, Transactor transactor) { this.sectionRegisterService = sectionRegisterService; @@ -71,11 +71,11 @@ protected Res register(User user, MultipartFile multipartFile, Multip } catch (IOException e) { throw new UncheckedIOException(e); } - if (!nullOrEmpty(ignoreExternalNames) && req instanceof RegisterRequest) { - updateToRemove((RegisterRequest) req, ignoreExternalNames); + if (!nullOrEmpty(ignoreExternalNames) && req instanceof BlockRegisterRequest) { + updateToRemove((BlockRegisterRequest) req, ignoreExternalNames); } - if (!nullOrEmpty(existingExternalNames) && req instanceof RegisterRequest) { - updateWithExisting((RegisterRequest) req, existingExternalNames); + if (!nullOrEmpty(existingExternalNames) && req instanceof BlockRegisterRequest) { + updateWithExisting((BlockRegisterRequest) req, existingExternalNames); } return transactor.transact("register", () -> service.apply(user, req)); } @@ -86,18 +86,23 @@ protected Res register(User user, MultipartFile multipartFile, Multip * @param request a block register request * @param existingExternalNames list of known existing external names */ - public void updateWithExisting(RegisterRequest request, List existingExternalNames) { - if (nullOrEmpty(existingExternalNames) || nullOrEmpty(request.getBlocks())) { + public void updateWithExisting(BlockRegisterRequest request, List existingExternalNames) { + if (request==null || nullOrEmpty(existingExternalNames) || nullOrEmpty(request.getLabware())) { return; } Set externalNamesUC = existingExternalNames.stream() .filter(Objects::nonNull) .map(String::toUpperCase) .collect(toSet()); - for (BlockRegisterRequest block : request.getBlocks()) { - if (block != null && !nullOrEmpty(block.getExternalIdentifier()) - && externalNamesUC.contains(block.getExternalIdentifier().toUpperCase())) { - block.setExistingTissue(true); + for (BlockRegisterLabware brl : request.getLabware()) { + if (brl==null || brl.getSamples()==null) { + continue; + } + for (BlockRegisterSample brs : brl.getSamples()) { + if (brs != null && !nullOrEmpty(brs.getExternalIdentifier()) + && externalNamesUC.contains(brs.getExternalIdentifier().toUpperCase())) { + brs.setExistingTissue(true); + } } } } @@ -107,18 +112,31 @@ public void updateWithExisting(RegisterRequest request, List existingExt * @param request block register request * @param externalNames external names to remove */ - public void updateToRemove(RegisterRequest request, List externalNames) { - if (nullOrEmpty(externalNames) || nullOrEmpty(request.getBlocks())) { + public void updateToRemove(BlockRegisterRequest request, List externalNames) { + if (request==null || nullOrEmpty(externalNames) || nullOrEmpty(request.getLabware())) { return; } Set ignoreUC = externalNames.stream() .filter(Objects::nonNull) .map(String::toUpperCase) .collect(toSet()); - List blocks = request.getBlocks().stream() - .filter(block -> block==null || block.getExternalIdentifier()==null || !ignoreUC.contains(block.getExternalIdentifier().toUpperCase())) - .toList(); - request.setBlocks(blocks); + List brls = new ArrayList<>(request.getLabware().size()); + for (BlockRegisterLabware brl : request.getLabware()) { + if (brl != null && !nullOrEmpty(brl.getSamples())) { + List samples = brl.getSamples().stream() + .filter(brs -> brs==null || brs.getExternalIdentifier()==null + || !ignoreUC.contains(brs.getExternalIdentifier().toUpperCase())) + .toList(); + if (samples.isEmpty()) { + continue; + } + if (samples.size() < brl.getSamples().size()) { + brl.setSamples(samples); + } + } + brls.add(brl); + } + request.setLabware(brls); } @Override diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterClashChecker.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterClashChecker.java index 97551fe9b..33869ab21 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterClashChecker.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterClashChecker.java @@ -33,10 +33,11 @@ public RegisterClashChecker(TissueRepo tissueRepo, SampleRepo sampleRepo, Operat this.lwRepo = lwRepo; } - public List findClashes(RegisterRequest request) { - Set externalNames = request.getBlocks().stream() - .filter(br -> !br.isExistingTissue()) - .map(BlockRegisterRequest::getExternalIdentifier) + public List findClashes(BlockRegisterRequest request) { + Set externalNames = request.getLabware().stream() + .flatMap(brl -> brl.getSamples().stream()) + .filter(brs -> !brs.isExistingTissue()) + .map(BlockRegisterSample::getExternalIdentifier) .collect(toSet()); if (externalNames.isEmpty()) { return List.of(); diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java deleted file mode 100644 index b40969c8a..000000000 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java +++ /dev/null @@ -1,181 +0,0 @@ -package uk.ac.sanger.sccp.stan.service.register; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.*; -import uk.ac.sanger.sccp.stan.service.*; -import uk.ac.sanger.sccp.stan.service.work.WorkService; - -import javax.persistence.EntityManager; -import java.util.*; - -/** - * @author dr6 - */ -@Service -public class RegisterServiceImp implements IRegisterService { - private final EntityManager entityManager; - private final RegisterValidationFactory validationFactory; - private final DonorRepo donorRepo; - private final TissueRepo tissueRepo; - private final SampleRepo sampleRepo; - private final SlotRepo slotRepo; - private final BioRiskRepo bioRiskRepo; - private final OperationTypeRepo opTypeRepo; - private final LabwareService labwareService; - private final OperationService operationService; - private final WorkService workService; - private final RegisterClashChecker clashChecker; - - @Autowired - public RegisterServiceImp(EntityManager entityManager, RegisterValidationFactory validationFactory, - DonorRepo donorRepo, TissueRepo tissueRepo, SampleRepo sampleRepo, SlotRepo slotRepo, - BioRiskRepo bioRiskRepo, OperationTypeRepo opTypeRepo, - LabwareService labwareService, OperationService operationService, WorkService workService, - RegisterClashChecker clashChecker) { - this.entityManager = entityManager; - this.validationFactory = validationFactory; - this.donorRepo = donorRepo; - this.tissueRepo = tissueRepo; - this.sampleRepo = sampleRepo; - this.slotRepo = slotRepo; - this.bioRiskRepo = bioRiskRepo; - this.opTypeRepo = opTypeRepo; - this.labwareService = labwareService; - this.operationService = operationService; - this.workService = workService; - this.clashChecker = clashChecker; - } - - @Override - public RegisterResult register(User user, RegisterRequest request) { - if (request.getBlocks().isEmpty()) { - return new RegisterResult(); // nothing to do - } - List clashes = clashChecker.findClashes(request); - if (!clashes.isEmpty()) { - return RegisterResult.clashes(clashes); - } - RegisterValidation validation = validationFactory.createRegisterValidation(request); - Collection problems = validation.validate(); - if (!problems.isEmpty()) { - throw new ValidationException("The register request could not be validated.", problems); - } - updateExistingTissues(request, validation); - return create(request, user, validation); - } - - public Map createDonors(RegisterRequest request, RegisterValidation validation) { - Map donors = new HashMap<>(); - for (BlockRegisterRequest block : request.getBlocks()) { - String donorName = block.getDonorIdentifier().toUpperCase(); - if (!donors.containsKey(donorName)) { - Donor donor = validation.getDonor(donorName); - if (donor.getId() == null) { - donor = donorRepo.save(donor); - } - donors.put(donorName, donor); - } - } - return donors; - } - - public Map createTissues(RegisterRequest request, RegisterValidation validation) { - Map donors = createDonors(request, validation); - Map tissueMap = new HashMap<>(request.getBlocks().size()); - for (BlockRegisterRequest block : request.getBlocks()) { - final String tissueKey = block.getExternalIdentifier().toUpperCase(); - Tissue existingTissue = validation.getTissue(tissueKey); - if (tissueMap.get(tissueKey)!=null) { - continue; - } - if (existingTissue!=null) { - tissueMap.put(tissueKey, existingTissue); - continue; - } - Donor donor = donors.get(block.getDonorIdentifier().toUpperCase()); - Hmdmc hmdmc; - if (block.getHmdmc()==null || block.getHmdmc().isEmpty()) { - hmdmc = null; - } else { - hmdmc = validation.getHmdmc(block.getHmdmc()); - if (hmdmc==null) { - throw new IllegalArgumentException("Unknown HuMFre number: "+block.getHmdmc()); - } - } - CellClass cellClass = validation.getCellClass(block.getCellClass()); - if (hmdmc==null && donor.getSpecies().requiresHmdmc() && cellClass.isHmdmcRequired()) { - throw new IllegalArgumentException("No HuMFre number given for tissue "+block.getExternalIdentifier()); - } - if (!donor.getSpecies().requiresHmdmc() && hmdmc!=null) { - throw new IllegalArgumentException("HuMFre number given for non-human tissue "+block.getExternalIdentifier()); - } - Tissue tissue = new Tissue(null, block.getExternalIdentifier(), block.getReplicateNumber().toLowerCase(), - validation.getSpatialLocation(block.getTissueType(), block.getSpatialLocation()), - donor, - validation.getMedium(block.getMedium()), - validation.getFixative(block.getFixative()), cellClass, - hmdmc, block.getSampleCollectionDate(), null); - tissueMap.put(tissueKey, tissueRepo.save(tissue)); - } - return tissueMap; - } - - /** - * Updates any existing tissues that now have a collection date - * @param request specification - * @param validation validation result to look up tissues - */ - public void updateExistingTissues(RegisterRequest request, RegisterValidation validation) { - List toUpdate = new ArrayList<>(); - for (BlockRegisterRequest brr : request.getBlocks()) { - if (brr.isExistingTissue() && brr.getSampleCollectionDate()!=null) { - Tissue tissue = validation.getTissue(brr.getExternalIdentifier()); - if (tissue!=null && tissue.getCollectionDate()==null) { - tissue.setCollectionDate(brr.getSampleCollectionDate()); - toUpdate.add(tissue); - } - } - } - if (!toUpdate.isEmpty()) { - tissueRepo.saveAll(toUpdate); - } - } - - public RegisterResult create(RegisterRequest request, User user, RegisterValidation validation) { - Map tissues = createTissues(request, validation); - - List labwareList = new ArrayList<>(request.getBlocks().size()); - OperationType opType = opTypeRepo.getByName("Register"); - BioState bioState = opType.getNewBioState(); - - List ops = new ArrayList<>(request.getBlocks().size()); - - for (BlockRegisterRequest block : request.getBlocks()) { - Tissue tissue = tissues.get(block.getExternalIdentifier().toUpperCase()); - Sample sample = sampleRepo.save(new Sample(null, null, tissue, bioState)); - LabwareType labwareType = validation.getLabwareType(block.getLabwareType()); - Labware labware = labwareService.create(labwareType); - Slot slot = labware.getFirstSlot(); - slot.getSamples().add(sample); - slot.setBlockSampleId(sample.getId()); - slot.setBlockHighestSection(block.getHighestSection()); - slot = slotRepo.save(slot); - entityManager.refresh(labware); - labwareList.add(labware); - final Operation op = operationService.createOperationInPlace(opType, user, slot, sample); - ops.add(op); - BioRisk bioRisk = validation.getBioRisk(block.getBioRiskCode()); - bioRiskRepo.recordBioRisk(sample, bioRisk, op.getId()); - } - - if (!ops.isEmpty() && validation.getWorks()!=null && !validation.getWorks().isEmpty()) { - workService.link(validation.getWorks(), ops); - } - - return new RegisterResult(labwareList); - } - -} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java index e502e1e1c..944808310 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java @@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; import uk.ac.sanger.sccp.stan.request.register.SectionRegisterRequest; import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.sanitiser.Sanitiser; @@ -36,7 +36,7 @@ public class RegisterValidationFactory { private final Validator xeniumLotValidator; private final Validator sectionValidator; private final Sanitiser thicknessSanitiser; - private final TissueFieldChecker tissueFieldChecker; + private final BlockFieldChecker blockFieldChecker; private final SlotRegionService slotRegionService; private final BioRiskService bioRiskService; private final WorkService workService; @@ -55,7 +55,7 @@ public RegisterValidationFactory(DonorRepo donorRepo, HmdmcRepo hmdmcRepo, Tissu @Qualifier("xeniumLotValidator") Validator xeniumLotValidator, @Qualifier("replicateValidator") Validator replicateValidator, @Qualifier("sectionValidator") Validator sectionValidator, - TissueFieldChecker tissueFieldChecker, + BlockFieldChecker blockFieldChecker, SlotRegionService slotRegionService, BioRiskService bioRiskService, WorkService workService) { this.donorRepo = donorRepo; this.hmdmcRepo = hmdmcRepo; @@ -77,16 +77,17 @@ public RegisterValidationFactory(DonorRepo donorRepo, HmdmcRepo hmdmcRepo, Tissu this.xeniumLotValidator = xeniumLotValidator; this.thicknessSanitiser = thicknessSanitiser; this.sectionValidator = sectionValidator; - this.tissueFieldChecker = tissueFieldChecker; + this.blockFieldChecker = blockFieldChecker; this.slotRegionService = slotRegionService; this.workService = workService; this.bioRiskService = bioRiskService; } - public RegisterValidation createRegisterValidation(RegisterRequest request) { - return new RegisterValidationImp(request, donorRepo, hmdmcRepo, ttRepo, ltRepo, mediumRepo, - fixativeRepo, tissueRepo, speciesRepo, cellClassRepo, donorNameValidation, externalNameValidation, replicateValidator, - tissueFieldChecker, bioRiskService, workService); + public RegisterValidation createBlockRegisterValidation(BlockRegisterRequest request) { + return new BlockRegisterValidationImp(request, donorRepo, hmdmcRepo, ttRepo, ltRepo, mediumRepo, + fixativeRepo, tissueRepo, speciesRepo, cellClassRepo, labwareRepo, donorNameValidation, + externalNameValidation, replicateValidator, externalBarcodeValidation, + blockFieldChecker, bioRiskService, workService); } public SectionRegisterValidation createSectionRegisterValidation(SectionRegisterRequest request) { diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/TissueFieldChecker.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/TissueFieldChecker.java deleted file mode 100644 index 5facee393..000000000 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/TissueFieldChecker.java +++ /dev/null @@ -1,82 +0,0 @@ -package uk.ac.sanger.sccp.stan.service.register; - -import org.springframework.stereotype.Service; -import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; - -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * Utility for checking that the description of tissue in a {@link BlockRegisterRequest} - * matches the information in an existing tissue. - * @author dr6 - */ -@Service -public class TissueFieldChecker { - /** Chains two functions, but skips the second if the first returns null */ - private static Function chain(Function ab, Function bc) { - return ab.andThen(b -> b==null ? null : bc.apply(b)); - } - enum Field { - DONOR(Tissue::getDonor, Donor::getDonorName, BlockRegisterRequest::getDonorIdentifier, "donor identifier"), - HMDMC(Tissue::getHmdmc, Hmdmc::getHmdmc, BlockRegisterRequest::getHmdmc, "HuMFre number"), - TTYPE(Tissue::getTissueType, TissueType::getName, BlockRegisterRequest::getTissueType, "tissue type"), - SL(Tissue::getSpatialLocation, SpatialLocation::getCode, BlockRegisterRequest::getSpatialLocation, "spatial location"), - REPLICATE(Tissue::getReplicate, BlockRegisterRequest::getReplicateNumber, "replicate number", false), - MEDIUM(Tissue::getMedium, Medium::getName, BlockRegisterRequest::getMedium, "medium"), - FIXATIVE(Tissue::getFixative, Fixative::getName, BlockRegisterRequest::getFixative, "fixative"), - COLLECTION_DATE(Tissue::getCollectionDate, BlockRegisterRequest::getSampleCollectionDate, "sample collection date", true), - CELL_CLASS(Tissue::getCellClass, CellClass::getName, BlockRegisterRequest::getCellClass, "cellular classification"), - ; - - private final Function tissueFunction; - private final Function brFunction; - private final String description; - private final boolean replaceMissing; - - Field(Function tissueFunction, Function brFunction, - String description, boolean replaceMissing) { - this.tissueFunction = tissueFunction; - this.brFunction = brFunction; - this.description = description; - this.replaceMissing = replaceMissing; - } - - Field(Function tissueFunction, Function xFunction, Function brFunction, - String description) { - this(chain(tissueFunction, xFunction), brFunction, description, false); - } - - public Object apply(Tissue tissue) { - return tissueFunction.apply(tissue); - } - public Object apply(BlockRegisterRequest br) { - return brFunction.apply(br); - } - } - - public void check(Consumer problemConsumer, BlockRegisterRequest br, Tissue tissue) { - for (Field field : Field.values()) { - Object oldValue = field.apply(tissue); - if (field.replaceMissing && oldValue==null) { - continue; - } - Object newValue = field.apply(br); - if (!match(oldValue, newValue)) { - problemConsumer.accept(String.format("Expected %s to be %s for existing tissue %s.", - field.description, oldValue, tissue.getExternalName())); - } - } - } - - public boolean match(Object oldValue, Object newValue) { - if (oldValue==null) { - return (newValue==null || newValue.equals("")); - } - if (oldValue instanceof String && newValue instanceof String) { - return ((String) oldValue).equalsIgnoreCase((String) newValue); - } - return oldValue.equals(newValue); - } -} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java index 27cc9f353..7d2ff86ae 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java @@ -2,7 +2,7 @@ import org.apache.poi.ss.usermodel.*; import org.springframework.web.multipart.MultipartFile; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; import uk.ac.sanger.sccp.stan.service.ValidationException; import java.io.IOException; @@ -13,7 +13,7 @@ /** * Reads a block registration request from an Excel file. */ -public interface BlockRegisterFileReader extends MultipartFileReader { +public interface BlockRegisterFileReader extends MultipartFileReader { /** The relevant sheet in the excel file to read. */ int SHEET_INDEX = 2; @@ -28,14 +28,16 @@ enum Column implements IColumn { Cell_class(Pattern.compile("cell(ular)?\\s*class(ification)?", Pattern.CASE_INSENSITIVE)), Bio_risk(Pattern.compile("bio\\w*\\s+risk.*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), HuMFre(Pattern.compile("humfre\\s*(number)?", Pattern.CASE_INSENSITIVE)), + Slot_address(Pattern.compile("(sample\\s*)?slot\\s*address.*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), Tissue_type, - External_identifier(Pattern.compile("external\\s*id.*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), + External_identifier(Pattern.compile("(sample\\s*)?external\\s*id.*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), Spatial_location(Integer.class, Pattern.compile("spatial\\s*location\\s*(number)?", Pattern.CASE_INSENSITIVE)), Replicate_number, Last_known_section(Integer.class, Pattern.compile("last.*section.*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), Labware_type, Fixative, Embedding_medium(Pattern.compile("(embedding)?\\s*medium", Pattern.CASE_INSENSITIVE)), + External_barcode(Pattern.compile("external\\s*barcode.*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), Comment(Void.class, Pattern.compile("(information|comment).*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL)), ; @@ -86,7 +88,7 @@ public boolean isRequired() { * @exception ValidationException the request is invalid * */ @Override - default RegisterRequest read(MultipartFile multipartFile) throws IOException, ValidationException { + default BlockRegisterRequest read(MultipartFile multipartFile) throws IOException, ValidationException { try (Workbook wb = WorkbookFactory.create(multipartFile.getInputStream())) { if (SHEET_INDEX < 0 || SHEET_INDEX >= wb.getNumberOfSheets()) { throw new ValidationException(List.of("Workbook does not have a worksheet at index "+SHEET_INDEX)); @@ -101,6 +103,6 @@ default RegisterRequest read(MultipartFile multipartFile) throws IOException, Va * @return a request read from the sheet * @exception ValidationException the request is invalid */ - RegisterRequest read(Sheet sheet) throws ValidationException; + BlockRegisterRequest read(Sheet sheet) throws ValidationException; } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java index 098e0147c..b1bf1354a 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java @@ -3,8 +3,8 @@ import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.stereotype.Service; -import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.model.Address; +import uk.ac.sanger.sccp.stan.request.register.*; import uk.ac.sanger.sccp.stan.service.ValidationException; import uk.ac.sanger.sccp.stan.service.register.filereader.BlockRegisterFileReader.Column; @@ -12,16 +12,18 @@ import java.nio.file.*; import java.time.LocalDate; import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Stream; -import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty; +import static uk.ac.sanger.sccp.utils.BasicUtils.repr; /** * @author dr6 */ @Service -public class BlockRegisterFileReaderImp extends BaseRegisterFileReader +public class BlockRegisterFileReaderImp extends BaseRegisterFileReader implements BlockRegisterFileReader { protected BlockRegisterFileReaderImp() { @@ -29,16 +31,15 @@ protected BlockRegisterFileReaderImp() { } @Override - protected RegisterRequest createRequest(Collection problems, List> rows) { - List blockRequests = rows.stream() - .map(row -> createBlockRequest(problems, row)) - .collect(toList()); - Set workNumbers = getUnique(rows.stream().map(row -> workNumberSet((String) row.get(Column.Work_number))), + protected BlockRegisterRequest createRequest(Collection problems, List> rows) { + List brlw = createLabwareRequests(problems, rows); + Set workNumberSet = getUnique(rows.stream().map(row -> workNumberSet((String) row.get(Column.Work_number))), () -> problems.add("All rows must list the same work numbers.")); if (!problems.isEmpty()) { throw new ValidationException("The file contents are invalid.", problems); } - return new RegisterRequest(blockRequests, nullOrEmpty(workNumbers) ? List.of() : new ArrayList<>(workNumbers)); + List workNumbers = (nullOrEmpty(workNumberSet) ? List.of() : new ArrayList<>(workNumberSet)); + return new BlockRegisterRequest(workNumbers, brlw); } /** @@ -48,61 +49,145 @@ protected RegisterRequest createRequest(Collection problems, List workNumberSet(String string) { - if (string == null) { - return null; + if (string != null) { + string = string.trim().toUpperCase(); } - string = string.trim().toUpperCase(); - if (string.isEmpty()) { + if (nullOrEmpty(string)) { return null; } String[] wns = string.replace(',',' ').split("\\s+"); - Set set = Arrays.stream(wns) + return Arrays.stream(wns) .filter(s -> !s.isEmpty()) .collect(toSet()); - return (set.isEmpty() ? null : set); } /** - * Creates the part of the request for registering one block from a row of data - * @param problems receptacle for problems found - * @param row the data from one row of the excel file - * @return a block register request based on the given row + * Parses the rows and groups them into labware. + * @param problems receptacle for problems + * @param rows rows from the file + * @return the labware requests + */ + public List createLabwareRequests(Collection problems, List> rows) { + Map>> groups = new LinkedHashMap<>(); + boolean anyMissing = false; + for (var row : rows) { + String xb = (String) row.get(Column.External_barcode); + if (nullOrEmpty(xb)) { + anyMissing = true; + continue; + } + groups.computeIfAbsent(xb.toUpperCase(), k -> new ArrayList<>()).add(row); + } + if (anyMissing) { + problems.add("Cannot process blocks without an external barcode."); + } else if (groups.isEmpty()) { + problems.add("No blocks requested."); + return List.of(); + } + return groups.values().stream().map(group -> toLabwareRequest(problems, group)).toList(); + } + + /** Stream the values in a particular column from a group of rows. */ + static Stream streamValues(Collection> rows, Column column) { + //noinspection unchecked + return rows.stream().map(row -> (T) row.get(column)); + } + + /** Gets the unique value from a column in a group of rows. */ + String uniqueRowValue(final Collection problems, + Collection> rows, Column column, + Supplier tooManyErrorSupplier) { + return getUniqueString(streamValues(rows, column), () -> problems.add(tooManyErrorSupplier.get())); + } + + /** + * Converts a list of rows into a labware request. + * @param problems receptacle for problems + * @param rows rows related to the same labware + * @return the labware request */ - public BlockRegisterRequest createBlockRequest(Collection problems, Map row) { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setDonorIdentifier((String) row.get(Column.Donor_identifier)); - br.setFixative((String) row.get(Column.Fixative)); - br.setHmdmc((String) row.get(Column.HuMFre)); - br.setBioRiskCode((String) row.get(Column.Bio_risk)); - br.setMedium((String) row.get(Column.Embedding_medium)); - br.setExternalIdentifier((String) row.get(Column.External_identifier)); - br.setSpecies((String) row.get(Column.Species)); - br.setCellClass((String) row.get(Column.Cell_class)); - br.setSampleCollectionDate((LocalDate) row.get(Column.Collection_date)); - br.setLifeStage(valueToLifeStage(problems, (String) row.get(Column.Life_stage))); - br.setTissueType((String) row.get(Column.Tissue_type)); + public BlockRegisterLabware toLabwareRequest(Collection problems, List> rows) { + String externalBarcode = ((String) rows.getFirst().get(Column.External_barcode)).toUpperCase(); + String labwareType = uniqueRowValue(problems, rows, Column.Labware_type, + () -> "Multiple labware types specified for external barcode "+repr(externalBarcode)+"."); + String medium = uniqueRowValue(problems, rows, Column.Embedding_medium, + () -> "Multiple media specified for external barcode "+repr(externalBarcode)+"."); + String fixative = uniqueRowValue(problems, rows, Column.Fixative, + () -> "Multiple fixatives specified for external barcode "+repr(externalBarcode)+"."); + List samples = rows.stream().map(row -> toSample(problems, row)).toList(); + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setExternalBarcode(externalBarcode); + brl.setLabwareType(labwareType); + brl.setMedium(medium); + brl.setFixative(fixative); + brl.setSamples(samples); + return brl; + } + + /** + * Reads the info about a single row into a sample request + * @param problems receptacle for problems + * @param row data from one row + * @return the sample information from the row + */ + public BlockRegisterSample toSample(Collection problems, Map row) { + BlockRegisterSample sample = new BlockRegisterSample(); + sample.setBioRiskCode((String) row.get(Column.Bio_risk)); + sample.setCellClass((String) row.get(Column.Cell_class)); + sample.setDonorIdentifier((String) row.get(Column.Donor_identifier)); + sample.setHmdmc((String) row.get(Column.HuMFre)); + sample.setLifeStage(valueToLifeStage(problems, (String) row.get(Column.Life_stage))); + sample.setReplicateNumber((String) row.get(Column.Replicate_number)); + sample.setSpecies((String) row.get(Column.Species)); + sample.setTissueType((String) row.get(Column.Tissue_type)); if (row.get(Column.Spatial_location)==null) { problems.add("Spatial location not specified."); } else { - br.setSpatialLocation((Integer) row.get(Column.Spatial_location)); + sample.setSpatialLocation((Integer) row.get(Column.Spatial_location)); } - br.setReplicateNumber((String) row.get(Column.Replicate_number)); if (row.get(Column.Last_known_section)==null) { problems.add("Last known section not specified."); } else { - br.setHighestSection((Integer) row.get(Column.Last_known_section)); + sample.setHighestSection((Integer) row.get(Column.Last_known_section)); + } + sample.setExternalIdentifier((String) row.get(Column.External_identifier)); + sample.setSampleCollectionDate((LocalDate) row.get(Column.Collection_date)); + sample.setAddresses(parseAddresses(problems, (String) row.get(Column.Slot_address))); + return sample; + } + + /** Parses a string into a list of slot addresses. */ + static List
parseAddresses(Collection problems, String string) { + if (string != null) { + string = string.trim(); + } + if (nullOrEmpty(string)) { + return List.of(); + } + String stringUpper = string.toUpperCase(); + String[] parts = stringUpper.replace(',',' ').split("\\s+"); + try { + return Arrays.stream(parts).map(Address::valueOf).toList(); + } catch (IllegalArgumentException e) { + // OK, try again with commas included } - br.setLabwareType((String) row.get(Column.Labware_type)); - return br; + parts = stringUpper.split("\\s+"); + try { + return Arrays.stream(parts).map(Address::valueOf).toList(); + } catch (IllegalArgumentException e) { + problems.add("Couldn't parse slot addresses: "+repr(string)); + } + return List.of(); } + /** * Test function to read an Excel file */ public static void main(String[] args) throws IOException { BlockRegisterFileReader r = new BlockRegisterFileReaderImp(); - final Path path = Paths.get("/Users/dr6/Desktop/regtest.xlsx"); - RegisterRequest request; + final Path path = Paths.get("/Users/dr6/Desktop/blockreg.xlsx"); + BlockRegisterRequest request; try (Workbook wb = WorkbookFactory.create(Files.newInputStream(path))) { request = r.read(wb.getSheetAt(SHEET_INDEX)); } catch (ValidationException e) { @@ -116,5 +201,4 @@ public static void main(String[] args) throws IOException { } System.out.println(request); } - } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java b/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java index 73667b8b5..dc5210f69 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java @@ -272,15 +272,15 @@ public Map loadSnapshots(Collection releases) { /** * Figures and sets the last section field for release entries. * The last section is only set on entries that specify a block. - * The last section is {@link Slot#getBlockHighestSection} + * The last section is {@link Sample#getBlockHighestSection} * @param entries the release entries under construction */ public void loadLastSection(Collection entries) { for (ReleaseEntry entry : entries) { - if (!entry.getSlot().isBlock() || !entry.getSlot().getBlockSampleId().equals(entry.getSample().getId())) { - continue; + Sample sample = entry.getSample(); + if (sample.isBlock()) { + entry.setLastSection(sample.getBlockHighestSection()); } - entry.setLastSection(entry.getSlot().getBlockHighestSection()); } } diff --git a/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java b/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java index f561f6b83..8fd0d987f 100644 --- a/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java +++ b/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java @@ -621,6 +621,13 @@ public static Stream stream(Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), false); } + /** + * Returns an iterable that emits the iterator from the given stream. + */ + public static Iterable iter(Stream stream) { + return stream::iterator; + } + /** * Does the given iterable contain any duplicates? * Duplication is checked using a hashset. diff --git a/src/main/resources/db/changelog/changelog-4.00.xml b/src/main/resources/db/changelog/changelog-4.00.xml new file mode 100644 index 000000000..b76da292a --- /dev/null +++ b/src/main/resources/db/changelog/changelog-4.00.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 9ba9786d8..f8c5470d1 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -44,4 +44,5 @@ + diff --git a/src/main/resources/schema.graphqls b/src/main/resources/schema.graphqls index afccad217..6e34e8e5a 100644 --- a/src/main/resources/schema.graphqls +++ b/src/main/resources/schema.graphqls @@ -224,6 +224,8 @@ type Sample { tissue: Tissue! """The state of this particular sample.""" bioState: BioState! + """For blocks, what is the highest section number already taken from this block?""" + blockHighestSection: Int } """A slot in a piece of labware, which may contain samples.""" @@ -236,10 +238,6 @@ type Slot { labwareId: Int! """The list of samples contained in this slot. May be empty.""" samples: [Sample!]! - """Is this slot a block of tissue? Blocks have different properties from sections.""" - block: Boolean! - """For blocks, what is the highest section number already taken from this block?""" - blockHighestSection: Int } type Labware { @@ -323,8 +321,10 @@ type ProbePanel { enabled: Boolean! } -"""A request to register a new block of tissue.""" -input BlockRegisterRequest { +"""A sample inside a block being registered.""" +input BlockRegisterSample { + """The slot addresses containing the sample.""" + addresses: [Address!]! """The string to use as the donor name.""" donorIdentifier: String! """The life stage of the donor.""" @@ -341,12 +341,6 @@ input BlockRegisterRequest { externalIdentifier: String! """The highest section already taken from the tissue block.""" highestSection: Int! - """The name of the type of labware containing the block.""" - labwareType: String! - """The medium used for the tissue.""" - medium: String! - """The fixative used for the tissue.""" - fixative: String! """The species of the donor.""" species: String! """The cellular classification of the tissue.""" @@ -359,10 +353,26 @@ input BlockRegisterRequest { bioRiskCode: String! } -"""A request to register one or more blocks of tissue.""" -input RegisterRequest { - blocks: [BlockRegisterRequest!]! +"""A request to register in a piece of labware containing one or more block-samples.""" +input BlockRegisterLabware { + """The name of the type of labware containing the block.""" + labwareType: String! + """The medium used for the tissue.""" + medium: String! + """The fixative used for the tissue.""" + fixative: String! + """The external barcode of the labware.""" + externalBarcode: String! + """The samples in this block.""" + samples: [BlockRegisterSample!]! +} + +"""A request to register new blocks of tissue.""" +input BlockRegisterRequest { + """The work numbers for the request.""" workNumbers: [String!]! + """The labware to register.""" + labware: [BlockRegisterLabware!]! } """Information about a section of tissue (already taken from some a block tracked elsewhere) to register.""" @@ -1818,16 +1828,26 @@ input TissueBlockRequest { """The input about a new block being created.""" input TissueBlockLabware { - """The original tissue barcode.""" - sourceBarcode: String! """The labware type for the new labware.""" labwareType: String! """The barcode of the new labware, if it is prebarcoded.""" preBarcode: String - """The comment (if any) associated with this operation.""" + """The contents (new samples) for the new labware.""" + contents: [TissueBlockContent!]! +} + +"""Info describing a new block in a tissue block request.""" +input TissueBlockContent { + """The barcode of the labware containing the source sample.""" + sourceBarcode: String! + """The id of the source sample.""" + sourceSampleId: Int! + """The addresses to place the sample in the new labware.""" + addresses: [Address!]! + """The id of a comment to record on the new block, if any.""" commentId: Int """The replicate number for the new block.""" - replicate: String! + replicate: String } """A request to transfer original sample into pots.""" @@ -2401,7 +2421,7 @@ type Mutation { """Log out; end the current login session.""" logout: String """Register blocks of tissue.""" - register(request: RegisterRequest!): RegisterResult! + registerBlocks(request: BlockRegisterRequest!): RegisterResult! """Register sections of tissue.""" registerSections(request: SectionRegisterRequest): RegisterResult! """Record planned operations.""" diff --git a/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java b/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java index 97ff02481..c1a792eab 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java @@ -118,6 +118,14 @@ public Tissue createTissue(Donor donor, SpatialLocation sl, String externalName, } + public Sample createBlockSample(Tissue tissue) { + BioState bs = bioStateRepo.getByName("Tissue"); + if (tissue==null) { + tissue = createTissue(null, "EXT1"); + } + return sampleRepo.save(Sample.newBlock(null, tissue, bs, 0)); + } + public Sample createSample(Tissue tissue, Integer section) { BioState bs = bioStateRepo.getByName("Tissue"); return createSample(tissue, section, bs); @@ -135,17 +143,17 @@ public Sample createSample(Tissue tissue, Integer section, BioState bioState) { } public Labware createTube(String barcode) { - LabwareType lt = ltRepo.getByName("Tube"); - Labware lw = labwareRepo.save(new Labware(null, barcode, lt, null)); - Slot slot = slotRepo.save(new Slot(null, lw.getId(), new Address(1,1), new ArrayList<>(), null, null)); - lw.getSlots().add(slot); - return lw; + return createTube(barcode, null); } - public Labware createBlock(String barcode, Sample sample) { - LabwareType lt = ltRepo.getByName("Proviasette"); + public Labware createTube(String barcode, Sample sample) { + LabwareType lt = ltRepo.getByName("Tube"); Labware lw = labwareRepo.save(new Labware(null, barcode, lt, null)); - Slot slot = slotRepo.save(new Slot(null, lw.getId(), new Address(1,1), new ArrayList<>(List.of(sample)), sample.getId(), 0)); + List samples = new ArrayList<>(); + if (sample != null) { + samples.add(sample); + } + Slot slot = slotRepo.save(new Slot(null, lw.getId(), new Address(1,1), samples)); lw.getSlots().add(slot); return lw; } @@ -159,8 +167,7 @@ public Labware createLabware(String barcode, LabwareType lt, Sample... samples) List slots = Address.stream(lt.getNumRows(), lt.getNumColumns()) .map(ad -> { Sample sample = sampleIter.hasNext() ? sampleIter.next() : null; - return new Slot(null, lw.getId(), ad, (sample==null ? List.of() : List.of(sample)), - null, null); + return new Slot(null, lw.getId(), ad, (sample==null ? List.of() : List.of(sample))); }) .collect(Collectors.toList()); slotRepo.saveAll(slots); @@ -175,7 +182,7 @@ public Labware createLabware(String barcode, LabwareType lt, Sample[][] samples) .map(ad -> { Sample[] sams = sampleArrayIter.hasNext() ? sampleArrayIter.next() : null; List samList = (sams==null ? List.of() : Arrays.asList(sams)); - return new Slot(null, lw.getId(), ad, samList, null, null); + return new Slot(null, lw.getId(), ad, samList); }) .collect(Collectors.toList()); slotRepo.saveAll(slots); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/EntityFactory.java b/src/test/java/uk/ac/sanger/sccp/stan/EntityFactory.java index f23718bff..e249d9311 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/EntityFactory.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/EntityFactory.java @@ -154,8 +154,7 @@ public static Labware getTube() { if (tube==null) { int lwId = 100; int slotId = 1001; - Slot slot = new Slot(slotId, lwId, new Address(1,1), new ArrayList<>(List.of(getSample())), - null, null); + Slot slot = new Slot(slotId, lwId, new Address(1,1), new ArrayList<>(List.of(getSample()))); tube = new Labware(lwId, "STAN-00"+lwId, getTubeType(), new ArrayList<>(List.of(slot))); } return tube; @@ -177,7 +176,7 @@ public static Labware makeEmptyLabware(LabwareType lt, String barcode) { int lwId = ++idCounter; final int[] slotId = { 10*lwId }; List slots = Address.stream(lt.getNumRows(), lt.getNumColumns()) - .map(ad -> new Slot(++slotId[0], lwId, ad, new ArrayList<>(), null, null)) + .map(ad -> new Slot(++slotId[0], lwId, ad, new ArrayList<>())) .collect(toList()); if (barcode==null) { barcode = "STAN-"+lwId; @@ -199,12 +198,8 @@ public static Labware makeLabware(LabwareType lt, Sample... samples) { return lw; } - public static Labware makeBlock(Sample sample) { - Labware lw = makeLabware(getTubeType(), sample); - Slot slot = lw.getFirstSlot(); - slot.setBlockSampleId(sample.getId()); - slot.setBlockHighestSection(0); - return lw; + public static Labware makeTube(Sample sample) { + return makeLabware(getTubeType(), sample); } public static OperationType makeOperationType(String name, BioState newBioState, OperationTypeFlag... flags) { diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java index bab1ae13e..ef3146a51 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java @@ -50,7 +50,7 @@ public class TestFileBlockRegister { ObjectMapper objectMapper; @MockBean - IRegisterService mockRegService; + IRegisterService mockRegService; @Test @Transactional @@ -81,10 +81,10 @@ public void testClashes() throws Exception { tester.setUser(user); Tissue tissue1 = creator.createTissue(null, "EXT1"); Tissue tissue2 = creator.createTissue(tissue1.getDonor(), "EXT2"); - Sample sample1 = creator.createSample(tissue1, null); - Sample sample2 = creator.createSample(tissue2, null); - Labware lw1 = creator.createBlock("STAN-X", sample1); - Labware lw2 = creator.createBlock("STAN-Y", sample2); + Sample sample1 = creator.createBlockSample(tissue1); + Sample sample2 = creator.createBlockSample(tissue2); + Labware lw1 = creator.createTube("STAN-X", sample1); + Labware lw2 = creator.createTube("STAN-Y", sample2); when(mockRegService.register(any(), any())).thenReturn(RegisterResult.clashes( List.of(new RegisterClash(tissue1, List.of(lw1)), new RegisterClash(tissue2, List.of(lw2))) )); @@ -100,7 +100,7 @@ public void testClashes() throws Exception { .containsExactlyInAnyOrder("EXT1", "EXT2"); for (var clash : clashes) { String bc = clash.get("tissue").get("externalName").equals("EXT1") ? "STAN-X" : "STAN-Y"; - assertEquals(List.of(Map.of("barcode", bc,"labwareType", Map.of("name", "Proviasette"))), + assertEquals(List.of(Map.of("barcode", bc,"labwareType", Map.of("name", "Tube"))), clash.get("labware")); } } @@ -117,12 +117,12 @@ public void testExistingExtNames() throws Exception { when(mockRegService.register(any(), any())).thenThrow(new ValidationException(List.of("Bad reg"))); var response = upload("testdata/block_reg_existing.xlsx", List.of("Ext17"), null, false); var map = objectMapper.readValue(response.getContentAsString(), Map.class); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(RegisterRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(BlockRegisterRequest.class); verify(mockRegService).register(eq(user), requestCaptor.capture()); - RegisterRequest request = requestCaptor.getValue(); - assertThat(request.getBlocks()).hasSize(2); - assertTrue(request.getBlocks().get(0).isExistingTissue()); - assertFalse(request.getBlocks().get(1).isExistingTissue()); + BlockRegisterRequest request = requestCaptor.getValue(); + assertThat(request.getLabware()).hasSize(2); + assertTrue(request.getLabware().get(0).getSamples().getFirst().isExistingTissue()); + assertFalse(request.getLabware().get(1).getSamples().getFirst().isExistingTissue()); assertEquals("Bad reg", getProblem(map)); } @@ -140,14 +140,15 @@ public void testIgnoreExtNames() throws Exception { when(mockRegService.register(any(), any())).thenThrow(new ValidationException(List.of("Bad reg"))); var response = upload("testdata/block_reg_existing.xlsx", null, List.of("Ext17"), false); var map = objectMapper.readValue(response.getContentAsString(), Map.class); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(RegisterRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(BlockRegisterRequest.class); verify(mockRegService).register(eq(user), requestCaptor.capture()); - RegisterRequest request = requestCaptor.getValue(); - assertThat(request.getBlocks()).hasSize(1); - BlockRegisterRequest br = request.getBlocks().getFirst(); - assertEquals("EXT18", br.getExternalIdentifier()); + BlockRegisterRequest request = requestCaptor.getValue(); + assertThat(request.getLabware()).hasSize(1); + BlockRegisterLabware brl = request.getLabware().getFirst(); + BlockRegisterSample brs = brl.getSamples().getFirst(); + assertEquals("EXT18", brs.getExternalIdentifier()); assertEquals("Bad reg", getProblem(map)); - assertEquals("risk1", br.getBioRiskCode()); + assertEquals("risk1", brs.getBioRiskCode()); } private MockHttpServletResponse upload(String filename) throws Exception { diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java index b307cdcca..8b4c3c727 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java @@ -40,7 +40,7 @@ public void testFindLatestOp() throws Exception { User user = entityCreator.createUser("user1"); tester.setUser(user); Object mutationResult = tester.post(mutation); - String barcode = chainGet(mutationResult, "data", "register", "labware", 0, "barcode"); + String barcode = chainGet(mutationResult, "data", "registerBlocks", "labware", 0, "barcode"); String query = tester.readGraphQL("findlatestop.graphql").replace("STAN-A1", barcode); Object result = tester.post(query); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java index 200ff3fae..98ea2a0ca 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java @@ -55,7 +55,7 @@ public void testHistory() throws Exception { tester.setUser(user); Map response = tester.post(mutation); - Map lwData = chainGet(response, "data", "register", "labware", 0); + Map lwData = chainGet(response, "data", "registerBlocks", "labware", 0); String barcode = chainGet(lwData, "barcode"); int sampleId = chainGet(lwData, "slots", 0, "samples", 0, "id"); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestOrientationQCMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestOrientationQCMutation.java index 6e2bca018..1af304768 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestOrientationQCMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestOrientationQCMutation.java @@ -42,8 +42,8 @@ public class TestOrientationQCMutation { @Transactional @Test public void testOrientationQC() throws Exception { - Sample sample = entityCreator.createSample(null, null); - Labware lw = entityCreator.createBlock("STAN-1", sample); + Sample sample = entityCreator.createBlockSample(null); + Labware lw = entityCreator.createTube("STAN-1", sample); Work work = entityCreator.createWork(null, null, null, null, null); OperationType opType = entityCreator.createOpType("Orientation QC", null, diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestPlanAndRecordSectionMutations.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestPlanAndRecordSectionMutations.java index 720a3f388..98ae9a50a 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestPlanAndRecordSectionMutations.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestPlanAndRecordSectionMutations.java @@ -50,12 +50,12 @@ public void testPlanAndRecordSection() throws Exception { entityCreator.createBioState("Fetal waste"); Sample[] blockSamples = { - entityCreator.createSample(entityCreator.createTissue(entityCreator.createDonor("DONOR1"), "TISSUE1"), null), - entityCreator.createSample(entityCreator.createTissue(entityCreator.createDonor("DONOR2"), "TISSUE2"), null), + entityCreator.createBlockSample(entityCreator.createTissue(entityCreator.createDonor("DONOR1"), "TISSUE1")), + entityCreator.createBlockSample(entityCreator.createTissue(entityCreator.createDonor("DONOR2"), "TISSUE2")), }; Labware[] sourceBlocks = { - entityCreator.createBlock("STAN-B70C", blockSamples[0]), - entityCreator.createBlock("STAN-B70D", blockSamples[1]), + entityCreator.createTube("STAN-B70C", blockSamples[0]), + entityCreator.createTube("STAN-B70D", blockSamples[1]), }; // Recording the plan @@ -272,8 +272,8 @@ private void testConfirm(Sample[] blockSamples, Labware[] sourceBlocks, String[] // Check that the source blocks' highest section numbers have been updated entityManager.refresh(sourceBlocks[0]); entityManager.refresh(sourceBlocks[1]); - assertEquals(14, sourceBlocks[0].getFirstSlot().getBlockHighestSection()); - assertEquals(17, sourceBlocks[1].getFirstSlot().getBlockHighestSection()); + assertEquals(14, sourceBlocks[0].getFirstSlot().getSamples().getFirst().getBlockHighestSection()); + assertEquals(17, sourceBlocks[1].getFirstSlot().getSamples().getFirst().getBlockHighestSection()); entityManager.flush(); entityManager.refresh(work); assertThat(work.getOperationIds()).hasSize(3); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java index e73d8bc58..d33a14b12 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java @@ -51,7 +51,7 @@ public void testRegister() throws Exception { tester.setUser(entityCreator.createUser("dr6")); String mutation = tester.readGraphQL("register.graphql").replace("SGP1", work.getWorkNumber()); Object result = tester.post(mutation); - Object data = chainGet(result, "data", "register"); + Object data = chainGet(result, "data", "registerBlocks"); assertThat(chainGetList(data, "clashes")).isEmpty(); String barcode = chainGet(data, "labware", 0, "barcode"); assertNotNull(barcode); @@ -61,7 +61,7 @@ public void testRegister() throws Exception { assertEquals("2021-02-03", tissueData.get("collectionDate")); result = tester.post(mutation); - data = chainGet(result, "data", "register"); + data = chainGet(result, "data", "registerBlocks"); assertThat(chainGetList(data, "labware")).isEmpty(); List> clashes = chainGet(data, "clashes"); assertThat(clashes).hasSize(1); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterOriginalSamplesMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterOriginalSamplesMutation.java index 8ebfd3eeb..262d0c54a 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterOriginalSamplesMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterOriginalSamplesMutation.java @@ -112,7 +112,7 @@ public void testRegisterOriginalSamples(boolean hasExternalName) throws Exceptio assertThat(bioRiskRepo.loadBioRiskForSampleId(sample.getId())).contains(risk1); Work work = entityCreator.createWork(null, null, null, null, null); - Labware block = testBlockProcessing(barcode, work); + Labware block = testBlockProcessing(barcode, work, sample.getId()); testSectioningBlock(block, work); lw.setDiscarded(false); lwRepo.save(lw); @@ -121,10 +121,11 @@ public void testRegisterOriginalSamples(boolean hasExternalName) throws Exceptio } - private Labware testBlockProcessing(String sourceBarcode, Work work) throws Exception { + private Labware testBlockProcessing(String sourceBarcode, Work work, Integer sourceSampleId) throws Exception { OperationType opType = entityCreator.createOpType("Block processing", null); String mutation = tester.readGraphQL("tissueblock.graphql") .replace("WORKNUMBER", work.getWorkNumber()) + .replace("999", String.valueOf(sourceSampleId)) .replace("BARCODE", sourceBarcode); Object result = tester.post(mutation); String destBarcode = chainGet(result, "data", "performTissueBlock", "labware", 0, "barcode"); @@ -141,7 +142,7 @@ private Labware testBlockProcessing(String sourceBarcode, Work work) throws Exce } private void testSectioningBlock(Labware block, Work work) throws Exception { - final Integer blockSampleId = block.getFirstSlot().getBlockSampleId(); + final Integer blockSampleId = block.getFirstSlot().getSamples().getFirst().getId(); String planMutation = tester.readGraphQL("plan_simple.graphql") .replace("BARCODE0", block.getBarcode()) .replace("55555", String.valueOf(blockSampleId)); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestReleaseMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestReleaseMutation.java index ca143cd0e..57ef425ad 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestReleaseMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestReleaseMutation.java @@ -81,13 +81,12 @@ public void testRelease() throws Exception { Work work1 = entityCreator.createWork(null, null, null, null, null); Donor donor = entityCreator.createDonor("DONOR1"); Tissue tissue = entityCreator.createTissue(donor, "TISSUE1"); - Sample sample = entityCreator.createSample(tissue, null); + Sample sample = entityCreator.createBlockSample(tissue); + sample.setBlockHighestSection(6); Sample sample1 = entityCreator.createSample(tissue, 1); LabwareType lwtype = entityCreator.createLabwareType("lwtype4", 1, 4); - Labware block = entityCreator.createBlock("STAN-001", sample); + Labware block = entityCreator.createTube("STAN-001", sample); Slot blockSlot = block.getFirstSlot(); - blockSlot.setBlockSampleId(sample.getId()); - blockSlot.setBlockHighestSection(6); blockSlot = slotRepo.save(blockSlot); block.getSlots().set(0, blockSlot); Labware lw = entityCreator.createLabware("STAN-002", lwtype, sample, sample, null, sample1); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanActionRepo.java b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanActionRepo.java index 44f682fd4..fdaf5e68f 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanActionRepo.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanActionRepo.java @@ -87,9 +87,9 @@ public void testFindMaxPlannedSection() { Labware lw = new Labware(null, "STAN-001A", any(labwareTypeRepo), null); labwareRepo.save(lw); - Slot slot1 = new Slot(null, lw.getId(), new Address(1,1), null, null, null); - Slot slot2 = new Slot(null, lw.getId(), new Address(1,2), null, null, null); - Slot slot3 = new Slot(null, lw.getId(), new Address(1,3), null, null, null); + Slot slot1 = new Slot(null, lw.getId(), new Address(1,1), null); + Slot slot2 = new Slot(null, lw.getId(), new Address(1,2), null); + Slot slot3 = new Slot(null, lw.getId(), new Address(1,3), null); slotRepo.save(slot1); slotRepo.save(slot2); slotRepo.save(slot3); @@ -104,8 +104,8 @@ public void testFindMaxPlannedSection() { @Test @Transactional public void testFindAllByDestinationLabwareId() { - Sample sample = entityCreator.createSample(entityCreator.createTissue(entityCreator.createDonor("DONOR1"), "TISSUE1"),null); - Labware sourceLabware = entityCreator.createBlock("STAN-000", sample); + Sample sample = entityCreator.createBlockSample(entityCreator.createTissue(entityCreator.createDonor("DONOR1"), "TISSUE1")); + Labware sourceLabware = entityCreator.createTube("STAN-000", sample); Slot sourceSlot = sourceLabware.getFirstSlot(); LabwareType lt = entityCreator.createLabwareType("2x2", 2, 2); Labware labware = entityCreator.createLabware("STAN-001", lt); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanOperationRepo.java b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanOperationRepo.java index 18e4d8e36..880f17194 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanOperationRepo.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestPlanOperationRepo.java @@ -31,7 +31,8 @@ public class TestPlanOperationRepo { @Transactional public void testFindAllByDestinationIdIn() { Tissue tissue = entityCreator.createTissue(entityCreator.createDonor("DONOR1"), "TISSUE1"); - Labware block = entityCreator.createBlock("STAN-BLOCK", entityCreator.createSample(tissue, null)); + Sample sample = entityCreator.createBlockSample(tissue); + Labware block = entityCreator.createTube("STAN-BLOCK", sample); Slot source = block.getFirstSlot(); LabwareType lt = entityCreator.createLabwareType("1x2", 1, 2); Labware lw1 = entityCreator.createLabware("STAN-1", lt); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestReleaseRepo.java b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestReleaseRepo.java index ebc8feb6f..48678e833 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestReleaseRepo.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestReleaseRepo.java @@ -84,9 +84,9 @@ public void testSaveRelease() { } private Release[] createReleases() { - Sample sample = createSample(); - Labware lw1 = entityCreator.createBlock("STAN-01", sample); - Labware lw2 = entityCreator.createBlock("STAN-02", sample); + Sample sample = entityCreator.createBlockSample(null); + Labware lw1 = entityCreator.createTube("STAN-01", sample); + Labware lw2 = entityCreator.createTube("STAN-02", sample); User user = entityCreator.createUser("user1"); ReleaseDestination destination = entityCreator.createReleaseDestination("Venus"); ReleaseRecipient recipient = entityCreator.createReleaseRecipient("Mekon"); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java index 958cee008..43d0eab38 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java @@ -2,71 +2,33 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.*; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.*; import uk.ac.sanger.sccp.stan.*; import uk.ac.sanger.sccp.stan.Matchers; import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.OperationResult; import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockLabware; +import uk.ac.sanger.sccp.stan.service.block.*; import uk.ac.sanger.sccp.stan.service.store.StoreService; -import uk.ac.sanger.sccp.stan.service.work.WorkService; -import uk.ac.sanger.sccp.utils.UCMap; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; +import java.util.List; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import static uk.ac.sanger.sccp.stan.Matchers.*; -import static uk.ac.sanger.sccp.utils.BasicUtils.coalesce; /** * Tests {@link BlockProcessingServiceImp} */ public class TestBlockProcessingService { @Mock - private LabwareValidatorFactory mockLwValFactory; + private BlockValidatorFactory mockBlockValFactory; @Mock - private Validator mockPrebarcodeValidator; - @Mock - private Validator mockReplicateValidator; - @Mock - private LabwareRepo mockLwRepo; - @Mock - private SlotRepo mockSlotRepo; - @Mock - private OperationTypeRepo mockOpTypeRepo; - @Mock - private OperationCommentRepo mockOpCommentRepo; - @Mock - private LabwareTypeRepo mockLtRepo; - @Mock - private BioStateRepo mockBsRepo; - @Mock - private TissueRepo mockTissueRepo; - @Mock - private SampleRepo mockSampleRepo; - @Mock - private MediumRepo mockMediumRepo; - @Mock - private CommentValidationService mockCommentValidationService; - @Mock - private OperationService mockOpService; - @Mock - private LabwareService mockLwService; - @Mock - private BioRiskService mockBioRiskService; - @Mock - private WorkService mockWorkService; + private BlockMakerFactory mockBlockMakerFactory; @Mock private StoreService mockStoreService; @Mock @@ -79,12 +41,8 @@ public class TestBlockProcessingService { @BeforeEach void setup() { mocking = MockitoAnnotations.openMocks(this); - service = spy(new BlockProcessingServiceImp(mockLwValFactory, mockPrebarcodeValidator, mockReplicateValidator, - mockLwRepo, mockSlotRepo, mockOpTypeRepo, mockOpCommentRepo, mockLtRepo, - mockBsRepo, mockTissueRepo, mockSampleRepo, mockMediumRepo, - mockCommentValidationService, mockOpService, mockLwService, mockBioRiskService, mockWorkService, - mockStoreService, - mockTransactor)); + service = spy(new BlockProcessingServiceImp(mockBlockValFactory, mockBlockMakerFactory, + mockStoreService, mockTransactor)); } @AfterEach @@ -93,31 +51,33 @@ void cleanup() throws Exception { } @ParameterizedTest - @CsvSource({"true,true", "true,false", "false,true"}) - public void testPerform(boolean succeeds, boolean discard) { - User user = EntityFactory.getUser(); - TissueBlockRequest request = new TissueBlockRequest(List.of()); - if (discard) { - request.setDiscardSourceBarcodes(List.of("STAN-A1")); - } + @CsvSource({ + "true, true", + "true,false", + "false,true", + "false,false", + }) + void testPerforms(boolean success, boolean discarding) { + TissueBlockRequest request = new TissueBlockRequest(); + request.setDiscardSourceBarcodes(discarding ? List.of("STAN-1", "STAN-2") : List.of()); Matchers.mockTransactor(mockTransactor); OperationResult opres; - if (succeeds) { - opres = new OperationResult(List.of(), List.of()); + if (success) { + opres = new OperationResult(List.of(new Operation()), List.of(new Labware())); doReturn(opres).when(service).performInsideTransaction(any(), any()); } else { opres = null; - doThrow(ValidationException.class).when(service).performInsideTransaction(any(), any()); + doThrow(new ValidationException(List.of("Bad request"))).when(service).performInsideTransaction(any(), any()); } + User user = EntityFactory.getUser(); - if (succeeds) { + if (success) { assertSame(opres, service.perform(user, request)); } else { - assertThrows(ValidationException.class, () -> service.perform(user, request)); + Matchers.assertValidationException(() -> service.perform(user, request), List.of("Bad request")); } - verify(mockTransactor).transact(any(), any()); verify(service).performInsideTransaction(user, request); - if (succeeds && discard) { + if (success && discarding) { verify(mockStoreService).discardStorage(user, request.getDiscardSourceBarcodes()); } else { verifyNoInteractions(mockStoreService); @@ -125,730 +85,71 @@ public void testPerform(boolean succeeds, boolean discard) { } @Test - public void testPerformInTransaction_noLabware() { - User user = EntityFactory.getUser(); - TissueBlockRequest request = new TissueBlockRequest(List.of()); - assertValidationException(() -> service.performInsideTransaction(user, request), - "The request could not be validated.", - "No labware specified in request."); - verifyNoInteractions(mockWorkService); - verify(service, never()).loadSources(any(), any()); - verify(service, never()).loadEntities(any(), any(), any(), any(), any(), anyBoolean(), any()); - verify(service, never()).checkPrebarcodes(any(), any(), any()); - verifyNoCreation(); + void testPerformInsideTransaction_noUser() { + TissueBlockRequest request = new TissueBlockRequest(); + assertThrows(NullPointerException.class, () -> service.performInsideTransaction(null, request), "User is null."); } @Test - public void testPerformInsideTransaction_invalid() { + void testPerformInsideTransaction_noRequest() { User user = EntityFactory.getUser(); - TissueBlockLabware block = new TissueBlockLabware("STAN-1", "lt", "1a"); - TissueBlockRequest request = new TissueBlockRequest(List.of(block), "SGP5", List.of("STAN-1")); - UCMap sources = UCMap.from(Labware::getBarcode, EntityFactory.getTube()); - LabwareType lt = EntityFactory.makeLabwareType(1,1, "lt"); - UCMap lwTypes = UCMap.from(LabwareType::getName, lt); - Work work = new Work(5, "SGP5", null, null, null, null, null, null); - Map commentMap = Map.of(50, new Comment(50, "Interesting", "science")); - - final String problem = "Bad sources."; - - stubValidation(sources, lwTypes, work, commentMap, problem); - - assertValidationException(() -> service.performInsideTransaction(user, request), - "The request could not be validated.", problem); - - verifyValidation(request, sources, lwTypes); + assertThrows(NullPointerException.class, () -> service.performInsideTransaction(user, null), "Request is null."); } @ParameterizedTest @ValueSource(booleans={false,true}) - public void testPerformInTransaction(boolean simple) { - String ltName = simple ? "lt" : "proviasette"; - LabwareType lt = EntityFactory.makeLabwareType(1,1, ltName); - Work work = (simple ? null : new Work(5, "SGP5", null, null, null, null, null, null)); - Comment comment = (simple ? null : new Comment(50, "Interesting", "science")); - TissueBlockLabware block = new TissueBlockLabware("STAN-1", ltName, "1a"); - TissueBlockRequest request = new TissueBlockRequest(List.of(block)); - Medium medium = null; - if (!simple) { - block.setCommentId(comment.getId()); - block.setPreBarcode("PREBC1"); - request.setWorkNumber(work.getWorkNumber()); - request.setDiscardSourceBarcodes(List.of("STAN-1")); - medium = new Medium(50, "OCT"); - when(mockMediumRepo.findByName("OCT")).thenReturn(Optional.of(medium)); - } - UCMap sources = UCMap.from(Labware::getBarcode, EntityFactory.getTube()); - UCMap ltMap = UCMap.from(LabwareType::getName, lt); - Map commentMap = Map.of(50, new Comment(50, "Hello", "stuff")); - - stubValidation(sources, ltMap, work, commentMap, null); - - List samples = List.of(EntityFactory.getSample()); - List dests = List.of(EntityFactory.makeLabware(lt, samples.getFirst())); - List ops = List.of(new Operation()); - doReturn(samples).when(service).createSamples(any(), any(), any()); - doReturn(dests).when(service).createDestinations(any(), any(), any()); - doReturn(ops).when(service).createOperations(any(), any(), any(), any(), any()); - doNothing().when(service).discardSources(any(), any()); - + void testPerformInsideTransaction(boolean valid) { + List problems = valid ? List.of() : List.of("Bad request"); + BlockValidator val = mock(BlockValidator.class); User user = EntityFactory.getUser(); - assertEquals(new OperationResult(ops, dests), service.performInsideTransaction(user, request)); - if (!simple) { - verify(mockMediumRepo).findByName("OCT"); - } else { - verifyNoInteractions(mockMediumRepo); - } - - verifyValidation(request, sources, ltMap); - verifyCreation(request, user, medium, sources, dests, commentMap, work, samples, ltMap, ops); - } - - private void stubValidation(UCMap sources, UCMap lwTypes, Work work, - Map commentMap, String problem) { - if (problem!=null) { - doAnswer(Matchers.addProblem(problem, sources)).when(service).loadSources(any(), any()); - } else { - doReturn(sources).when(service).loadSources(any(), any()); - } - doReturn(work).when(mockWorkService).validateUsableWork(any(), any()); - doReturn(lwTypes).when(service).loadEntities(any(), any(), any(), eq("Labware type"), any(), anyBoolean(), any()); - doNothing().when(service).checkPrebarcodes(any(), any(), any()); - doReturn(commentMap).when(service).loadComments(any(), any()); - doNothing().when(service).checkReplicates(any(), any(), any()); - doNothing().when(service).checkDiscardBarcodes(any(), any()); - } - - private void verifyValidation(TissueBlockRequest request, UCMap sources, UCMap lwTypes) { - //noinspection unchecked - ArgumentCaptor> problemsCaptor = ArgumentCaptor.forClass(Set.class); - LabwareType lt = EntityFactory.getTubeType(); - final List ltList = List.of(lt); - TissueBlockLabware block = request.getLabware().getFirst(); - - verify(service).loadSources(problemsCaptor.capture(), same(request)); - final Set problems = problemsCaptor.getValue(); - verify(mockWorkService).validateUsableWork(same(problems), eq(request.getWorkNumber())); - when(mockLtRepo.findAllByNameIn(any())).thenReturn(ltList); - verify(service).loadEntities(same(problems), same(request), - functionLike(TissueBlockLabware::getLabwareType, block), eq("Labware type"), - functionLike(LabwareType::getName, lt), eq(true), - functionGiving(null, ltList)); - verify(service).checkPrebarcodes(same(problems), same(request), same(lwTypes)); - verify(service).loadComments(same(problems), same(request)); - verify(service).checkReplicates(same(problems), same(request), same(sources)); - verify(service).checkDiscardBarcodes(same(problems), same(request)); - } - - private void verifyNoCreation() { - verify(service, never()).createSamples(any(), any(), any()); - verify(service, never()).createDestinations(any(), any(), any()); - verify(service, never()).createOperations(any(), any(), any(), any(), any()); - verifyNoInteractions(mockBioRiskService); - verify(mockWorkService, never()).link(any(Work.class), any()); - verify(service, never()).discardSources(any(), any()); - } - - private void verifyCreation(TissueBlockRequest request, User user, Medium medium, UCMap sources, List dests, - Map commentMap, Work work, - List samples, UCMap ltMap, List ops) { - verify(service).createSamples(same(request), same(sources), same(medium)); - verify(service).createDestinations(same(request), same(samples), same(ltMap)); - verify(service).createOperations(same(request), same(user), same(sources), same(dests), same(commentMap)); - verify(mockBioRiskService).copyOpSampleBioRisks(same(ops)); - if (work!=null) { - verify(mockWorkService).link(same(work), same(ops)); - } else { - verify(mockWorkService, never()).link(any(Work.class), any()); - } - verify(service).discardSources(any(), any()); - } - - @ParameterizedTest - @MethodSource("loadSourcesArgs") - public void testLoadSources(Collection barcodes, List labware, BioState bs, - Collection valErrors, - Collection expectedProblems) { - TissueBlockRequest request = new TissueBlockRequest( - barcodes.stream() - .map(bc -> { - TissueBlockLabware block = new TissueBlockLabware(); - block.setSourceBarcode(bc); - return block; - }) - .collect(toList()) - ); - when(mockBsRepo.getByName("Original sample")).thenReturn(bs); - LabwareValidator val = mock(LabwareValidator.class); - when(mockLwValFactory.getValidator()).thenReturn(val); - when(val.getErrors()).thenReturn(valErrors); - when(val.loadLabware(any(), any())).thenReturn(labware); - when(val.getLabware()).thenReturn(labware); - - final List problems = new ArrayList<>(expectedProblems.size()); - UCMap lwMap = service.loadSources(problems, request); - - assertThat(lwMap).hasSize(labware.size()); - labware.forEach(lw -> assertSame(lw, lwMap.get(lw.getBarcode()))); - - Set nonNullBarcodes = barcodes.stream() - .filter(bc -> bc!=null && !bc.isEmpty()) - .collect(toSet()); - - verify(val).loadLabware(mockLwRepo, nonNullBarcodes); - verify(val).setSingleSample(true); - verify(val).validateSources(); - verify(val).validateBioState(bs); - - assertThat(problems).containsExactlyInAnyOrderElementsOf(expectedProblems); - } - - static Stream loadSourcesArgs() { - LabwareType lt = EntityFactory.getTubeType(); - Labware lw1 = EntityFactory.makeLabware(lt); - lw1.setBarcode("STAN-1"); - Labware lw2 = EntityFactory.makeLabware(lt); - lw2.setBarcode("STAN-2"); - BioState bs = EntityFactory.getBioState(); - - return Arrays.stream(new Object[][]{ - {List.of("STAN-404", "STAN-405"), List.of(), bs, List.of("Unknown barcodes: STAN-404, STAN-405"), - List.of("Unknown barcodes: STAN-404, STAN-405")}, - {List.of("STAN-1", "STAN-2", ""), List.of(lw1, lw2), bs, List.of(), - List.of("Source barcode missing.")}, - {Arrays.asList("STAN-404", null, "STAN-1"), List.of(lw1), bs, List.of("Bad barcode: STAN-404"), - List.of("Bad barcode: STAN-404", "Source barcode missing.")}, - }).map(Arguments::of); - } - - @ParameterizedTest - @MethodSource("checkPrebarcodesArgs") - public void testCheckPrebarcodes(List prebarcodes, List lwTypeNames, UCMap ltMap, - List existingLabware, boolean existingFromExternal, - Collection expectedProblems) { - when(mockPrebarcodeValidator.validate(any(), any())).then(invocation -> { - String barcode = invocation.getArgument(0); - Consumer problemConsumer = invocation.getArgument(1); - if (barcode!=null && barcode.indexOf('!') < 0) { - return true; - } - problemConsumer.accept("Bad barcode: "+barcode); - return false; - }); - when(mockLwRepo.findByBarcodeIn(any())).thenReturn(existingFromExternal ? List.of() : existingLabware); - when(mockLwRepo.findByExternalBarcodeIn(any())).thenReturn(existingFromExternal ? existingLabware : List.of()); - - final List problems = new ArrayList<>(expectedProblems.size()); - final Iterator lwTypeNameIter = lwTypeNames.iterator(); - TissueBlockRequest request = new TissueBlockRequest( - prebarcodes.stream() - .map(bc -> { - TissueBlockLabware block = new TissueBlockLabware(); - block.setPreBarcode(bc); - block.setLabwareType(lwTypeNameIter.next()); - return block; - }) - .collect(toList()) - ); - service.checkPrebarcodes(problems, request, ltMap); - - assertThat(problems).containsExactlyInAnyOrderElementsOf(expectedProblems); - Set prebarcodeSet = prebarcodes.stream() - .filter(bc -> bc!=null && !bc.isEmpty()) - .peek(bc -> verify(mockPrebarcodeValidator).validate(eq(bc), any())) - .map(String::toUpperCase) - .collect(toSet()); - if (!prebarcodeSet.isEmpty()) { - verify(mockLwRepo).findByBarcodeIn(prebarcodeSet); - if (existingFromExternal) { - verify(mockLwRepo).findByExternalBarcodeIn(prebarcodeSet); - } - } - } - - static Stream checkPrebarcodesArgs() { - LabwareType pretube = EntityFactory.makeLabwareType(1,1, "Pretube"); - pretube.setPrebarcoded(true); - UCMap ltMap = UCMap.from(LabwareType::getName, EntityFactory.getTubeType(), pretube); - Labware existingLw = EntityFactory.makeEmptyLabware(pretube); - existingLw.setBarcode("EXISTING"); - existingLw.setExternalBarcode("EXISTING"); - List existingLwList = List.of(existingLw); - return Arrays.stream(new Object[][]{ - {Arrays.asList("ALPHA", null, "BETA", null), List.of("Pretube", "Tube", "Pretube", "Tube"), - ltMap, List.of(), false, List.of()}, - {Arrays.asList("ALPHA", "BETA"), List.of("Tube", "tube"), ltMap, List.of(), false, - List.of("A barcode is not expected for labware type [Tube].")}, - {Arrays.asList(null, null), List.of("pretube", "pretube"), ltMap, List.of(), false, - List.of("A barcode is required for labware type [Pretube].")}, - {Arrays.asList(null, "ALPHA"), List.of("", "foo"), ltMap, List.of(), false, List.of()}, - {List.of("ALPHA", "Beta", "Alpha"), List.of("Pretube", "Pretube", "Pretube"), ltMap, List.of(), false, - List.of("Barcode specified multiple times: [ALPHA]")}, - {Arrays.asList("Existing", "Alpha", null), List.of("Pretube", "Pretube", "Tube"), ltMap, - existingLwList, false, List.of("Barcode already in use: [EXISTING]")}, - {Arrays.asList("Existing", "Alpha", null), List.of("Pretube", "Pretube", "Tube"), ltMap, - existingLwList, true, List.of("External barcode already in use: [EXISTING]")}, - {Arrays.asList(null, "Alpha", "Beta!"), List.of("Tube", "Pretube", "Pretube"), ltMap, - List.of(), false, List.of("Bad barcode: Beta!")}, - {List.of("", "Alpha", "ALPHA", "gamma", "Beta!", "existing"), - List.of("Pretube", "Pretube", "Pretube", "Tube", "Pretube", "Pretube"), - ltMap, existingLwList, false, - List.of("A barcode is not expected for labware type [Tube].", - "A barcode is required for labware type [Pretube].", - "Barcode specified multiple times: [ALPHA]", - "Bad barcode: Beta!", - "Barcode already in use: [EXISTING]")}, - - }).map(Arguments::of); - } - - @ParameterizedTest - @MethodSource("loadCommentsArgs") - public void testLoadComments(List ids, List comments, List errors) { - final List receivedCommentIds = new ArrayList<>(ids.size()); - when(mockCommentValidationService.validateCommentIds(any(), any())) - .then(invocation -> { - Stream commentIds = invocation.getArgument(1); - commentIds.forEach(receivedCommentIds::add); - if (!errors.isEmpty()) { - Collection problems = invocation.getArgument(0); - problems.addAll(errors); - } - return comments; - }); - List problems = new ArrayList<>(errors.size()); - TissueBlockRequest request = new TissueBlockRequest( - ids.stream() - .map(id -> { - TissueBlockLabware block = new TissueBlockLabware(); - block.setCommentId(id); - return block; - }) - .collect(toList()) - ); - Map map = service.loadComments(problems, request); - assertThat(map.values()).containsExactlyInAnyOrderElementsOf(comments); - for (Comment comment : comments) { - assertSame(comment, map.get(comment.getId())); - } - assertThat(problems).containsExactlyElementsOf(errors); - verify(mockCommentValidationService).validateCommentIds(any(), any()); - List expectedCommentIds = ids.stream() - .filter(Objects::nonNull) - .collect(toList()); - assertThat(receivedCommentIds).containsExactlyInAnyOrderElementsOf(expectedCommentIds); - } - - static Stream loadCommentsArgs() { - Comment com1 = new Comment(1, "Banana", "Fruit"); - Comment com2 = new Comment(2, "Whale", "Insect"); - List comments = List.of(com1, com2); - return Arrays.stream(new Object[][] { - {List.of(), List.of(), List.of()}, - {Arrays.asList(null, null), List.of(), List.of()}, - {Arrays.asList(null, 1, 2, 1, null), comments, List.of()}, - {Arrays.asList(null, 1, 2, 1, 5), comments, List.of("Bad comment id: 5")}, - }).map(Arguments::of); - } - - @ParameterizedTest - @MethodSource("loadEntitiesArgs") - public void testLoadEntities(TissueBlockRequest request, Function fieldGetter, - String fieldName, Function entityFieldGetter, boolean required, - Collection entities, Collection expectedProblems) { - final Set lookedUp = new HashSet<>(); - Function, Collection> lookupFunction = strings -> { - lookedUp.addAll(strings); - return entities; - }; - final List problems = new ArrayList<>(expectedProblems.size()); - var result = service.loadEntities(problems, request, fieldGetter, fieldName, entityFieldGetter, - required, lookupFunction); - assertThat(result.values()).containsExactlyInAnyOrderElementsOf(entities); - for (var entity : entities) { - assertSame(entity, result.get(entityFieldGetter.apply(entity))); - } - assertThat(problems).containsExactlyInAnyOrderElementsOf(expectedProblems); - Set expectedFieldValues = request.getLabware().stream() - .map(fieldGetter) - .filter(s -> s!=null && !s.isEmpty()) - .collect(toSet()); - assertEquals(lookedUp, expectedFieldValues); - } - - static Stream loadEntitiesArgs() { - LabwareType lt1 = EntityFactory.makeLabwareType(1,1, "lt1"); - LabwareType lt2 = EntityFactory.makeLabwareType(1,1, "lt2"); - List lwTypes = List.of(lt1, lt2); - Function fieldGetter = TissueBlockLabware::getLabwareType; - Function entityFieldGetter = LabwareType::getName; - final String fieldName = "Labware type"; - return Arrays.stream(new Object[][]{ - {lwTypesToRequest("lt1", "lt2", "LT1"), fieldGetter, fieldName, entityFieldGetter, - true, lwTypes, List.of()}, - {lwTypesToRequest("lt1", "lt2", "", null), fieldGetter, fieldName, entityFieldGetter, - false, lwTypes, List.of()}, - {lwTypesToRequest("lt1", "lt2", ""), fieldGetter, fieldName, entityFieldGetter, - true, lwTypes, List.of("Labware type missing.")}, - {lwTypesToRequest(""), fieldGetter, fieldName, entityFieldGetter, - false, List.of(), List.of()}, - {lwTypesToRequest("lt1", "lt2", null), fieldGetter, fieldName, entityFieldGetter, - true, lwTypes, List.of("Labware type missing.")}, - {lwTypesToRequest("LT1", "LT4", "LT5", "lt5"), fieldGetter, fieldName, entityFieldGetter, - true, List.of(lt1), List.of("Labware type unknown: [\"LT4\", \"LT5\"]")}, - {lwTypesToRequest("LT1", "LT4", null), fieldGetter, fieldName, entityFieldGetter, - true, List.of(lt1), List.of("Labware type unknown: [\"LT4\"]", "Labware type missing.")}, - }).map(Arguments::of); - } - - private static TissueBlockRequest lwTypesToRequest(String... labwareTypeNames) { - return new TissueBlockRequest(Arrays.stream(labwareTypeNames) - .map(ltName -> { - TissueBlockLabware block = new TissueBlockLabware(); - block.setLabwareType(ltName); - return block; - }).collect(toList())); - } - - @ParameterizedTest - @MethodSource("checkReplicatesArgs") - public void testCheckReplicates(TissueBlockRequest request, UCMap sources, - Collection existingTissues, Collection expectedProblems) { - when(mockReplicateValidator.validate(any(), any())).then(invocation -> { - String string = invocation.getArgument(0); - if (string.indexOf('!') < 0) { - return true; - } - Consumer addProblem = invocation.getArgument(1); - addProblem.accept("Bad rep: "+string); - return false; - }); - when(mockTissueRepo.findByDonorIdAndSpatialLocationIdAndReplicate(anyInt(), anyInt(), any())).then(invocation -> { - int donorId = invocation.getArgument(0); - int slId = invocation.getArgument(1); - String rep = invocation.getArgument(2); - return existingTissues.stream() - .filter(t -> t.getDonor().getId()==donorId && t.getSpatialLocation().getId()==slId && rep.equalsIgnoreCase(t.getReplicate())) - .collect(toList()); - }); - final List problems = new ArrayList<>(expectedProblems.size()); - service.checkReplicates(problems, request, sources); - assertThat(problems).containsExactlyInAnyOrderElementsOf(expectedProblems); - } - - static Stream checkReplicatesArgs() { - Donor d1 = new Donor(1, "DONOR1", null, null); - Donor d2 = new Donor(2, "DONOR2", null, null); - TissueType tt = new TissueType(1, "Arm", "ARM"); - SpatialLocation sl1 = new SpatialLocation(101, "SL1", 1, tt); - SpatialLocation sl2 = new SpatialLocation(102, "SL2", 2, tt); - Tissue t1A = makeTissue(1, "t1A", null, sl1, d1); - Tissue t1B = makeTissue(2, "t1B", "1b", sl1, d1); - Tissue t1C = makeTissue(3, "t1C", "1c", sl2, d1); - Tissue t2A = makeTissue(4, "t2A", "2a", sl1, d2); - Tissue t2B = makeTissue(5, "t2B", "2b", sl2, d2); - final List tissues = List.of(t1A, t1B, t1C, t2A, t2B); - // Cases: - LabwareType lt = EntityFactory.getTubeType(); - BioState bs = EntityFactory.getBioState(); - Labware lw1 = EntityFactory.makeLabware(lt, new Sample(1, null, t1A, bs)); - lw1.setBarcode("STAN-1"); - Labware lw2 = EntityFactory.makeLabware(lt, new Sample(2, null, t2A, bs)); - lw2.setBarcode("STAN-2"); - UCMap sources = UCMap.from(Labware::getBarcode, lw1, lw2); - - return Arrays.stream(new Object[][] { - {requestForReplicates("STAN-1", "5", "STAN-1", "6", "STAN-1", "1c", "STAN-2", "2a")}, - {requestForReplicates("STAN-1", "5", "STAN-1", "5", "STAN-2", "6", "Stan-2", "6", "STAN-2", "7"), - List.of("Same replicate specified multiple times: [{Donor: DONOR1, Tissue type: Arm, Spatial location: 1, Replicate: 5}]", - "Replicate numbers must match the source replicate number where present.")}, - {requestForReplicates("STAN-1", null), "Missing replicate for some blocks."}, - {requestForReplicates("STAN-1", ""), "Missing replicate for some blocks."}, - {requestForReplicates("STAN-1", "5", "Stan-1", "1b"), "Replicate already exists in the database: " + - "[{Donor: DONOR1, Tissue type: Arm, Spatial location: 1, Replicate: 1b}]"}, - {requestForReplicates(null, "1b", "STAN-2", "5F", "Stan-2", "5A", "Stan-1", null, - "STAN-1", "1F", "STAN-1", "1f", "STAN-1", "1b"), - List.of("Same replicate specified multiple times: [{Donor: DONOR1, Tissue type: Arm, Spatial location: 1, Replicate: 1f}]", - "Missing replicate for some blocks.", - "Replicate numbers must match the source replicate number where present.", - "Replicate already exists in the database: [{Donor: DONOR1, Tissue type: Arm, Spatial location: 1, Replicate: 1b}]")}, - }).map(arr -> { - List problems; - if (arr.length < 2) { - problems = List.of(); - } else if (arr[1] instanceof String) { - problems = List.of(arr[1]); - } else { - problems = (List) arr[1]; - } - return Arguments.of(arr[0], sources, tissues, problems); - }); - } - - static Tissue makeTissue(Integer id, String name, String replicate, SpatialLocation sl, Donor d) { - return new Tissue(id, name, replicate, sl, d, null, null, null, null, null, null); - } - - static TissueBlockRequest requestForReplicates(String... barcodesAndReplicates) { - assert barcodesAndReplicates.length % 2 == 0; - List blocks = new ArrayList<>(barcodesAndReplicates.length/2); - for (int i = 0; i < barcodesAndReplicates.length; i += 2) { - TissueBlockLabware block = new TissueBlockLabware(); - block.setSourceBarcode(barcodesAndReplicates[i]); - block.setReplicate(barcodesAndReplicates[i+1]); - blocks.add(block); - } - return new TissueBlockRequest(blocks); - } - - @ParameterizedTest - @CsvSource(value = { - ";;", - "; Stan-1/Stan-2;", - "STAN-1; Stan-1/Stan-2/STAN-2;", - "/Stan-1;Stan-1;A null or empty string was supplied as a barcode to discard.", - "Stan-1/Stan-4/Stan-5; STAN-1/STAN-1/STAN-2; The given list of barcodes to discard includes barcodes " + - "that are not specified as source barcodes in this request: [\"Stan-4\", \"Stan-5\"]", - "Stan-1//Stan-5; STAN-1/STAN-2; The given list of barcodes to discard includes a barcode that is not " + - "specified as a source barcode in this request: [\"Stan-5\"]/A null or empty string was " + - "supplied as a barcode to discard.", - }, delimiter=';') - public void testCheckDiscardBarcodes(String discardBarcodesJoined, String sourceBarcodesJoined, String expectedProblemsJoined) { - String[] discardBarcodes = (discardBarcodesJoined==null ? new String[0] : discardBarcodesJoined.split("/")); - String[] sourceBarcodes = (sourceBarcodesJoined==null ? new String[0] : sourceBarcodesJoined.split("/")); - String[] expectedProblems = (expectedProblemsJoined==null ? new String[0] : expectedProblemsJoined.split("/")); - - List blocks = Arrays.stream(sourceBarcodes) - .map(bc -> { - TissueBlockLabware block = new TissueBlockLabware(); - block.setSourceBarcode(bc); - return block; - }).collect(toList()); - TissueBlockRequest request = new TissueBlockRequest(blocks, null, Arrays.asList(discardBarcodes)); - - final List problems = new ArrayList<>(expectedProblems.length); - service.checkDiscardBarcodes(problems, request); - assertThat(problems).containsExactlyInAnyOrder(expectedProblems); - } - - @Test - public void testCreateSamples() { - BioState bs = EntityFactory.getBioState(); - when(mockBsRepo.getByName("Tissue")).thenReturn(bs); - LabwareType lt = EntityFactory.getTubeType(); - Labware lw1 = EntityFactory.makeEmptyLabware(lt); - lw1.setBarcode("STAN-1"); - Labware lw2 = EntityFactory.makeEmptyLabware(lt); - lw2.setBarcode("STAN-2"); - Medium oct = new Medium(50, "OCT"); - - TissueBlockLabware block1 = new TissueBlockLabware("STAN-1", "lt", "1a"); - TissueBlockLabware block2 = new TissueBlockLabware("STAN-2", "proviasette", "2b"); - TissueBlockRequest request = new TissueBlockRequest(List.of(block1, block2)); - - Sample sam1 = EntityFactory.getSample(); - Sample sam2 = new Sample(10, "20", sam1.getTissue(), bs); - - doReturn(sam1, sam2).when(service).createSample(any(), any(), any(), any()); - - List samples = service.createSamples(request, UCMap.from(Labware::getBarcode, lw1, lw2), oct); - - assertThat(samples).containsExactly(sam1, sam2); - verify(service, times(2)).createSample(any(), any(), any(), any()); - verify(service).createSample(block1, lw1, bs, null); - verify(service).createSample(block2, lw2, bs, oct); - } - - @ParameterizedTest - @CsvSource({ - ",,None,,false", - ",,OCT,OCT,false", - "1a,1a,OCT,OCT,false", - ",1a,None,,true", - "1a,1a,None,OCT,true", - "1a,1b,None,OCT,true", - }) - public void testGetOrCreateTissue(String oldRep, String newRep, String oldMedium, String newMedium, boolean save) { - if (save) { - when(mockTissueRepo.save(any())).thenAnswer(invocation -> { - Tissue tis = invocation.getArgument(0); - tis.setId(50); - return tis; - }); - } - Medium med0 = new Medium(100, oldMedium); - Medium med1; - if (newMedium==null) { - med1 = null; - } else if (newMedium.equalsIgnoreCase(oldMedium)) { - med1 = med0; + TissueBlockRequest request = new TissueBlockRequest(); + BlockMaker maker; + OperationResult opres; + when(mockBlockValFactory.createBlockValidator(any())).thenReturn(val); + List lwData; + Medium medium; + BioState bs; + Work work; + OperationType opType; + if (!valid) { + doThrow(new ValidationException(problems)).when(val).raiseError(); + maker = null; + opres = null; + lwData = null; + medium = null; + bs = null; + work = null; + opType = null; + } else { + maker = mock(BlockMaker.class); + when(mockBlockMakerFactory.createBlockMaker(any(), any(), any(), any(), any(), any(), any())).thenReturn(maker); + opres = new OperationResult(List.of(new Operation()), List.of(new Labware())); + when(maker.record()).thenReturn(opres); + lwData = List.of(new BlockLabwareData(new TissueBlockLabware())); + medium = EntityFactory.getMedium(); + bs = EntityFactory.getBioState(); + work = EntityFactory.makeWork("SGP1"); + opType = EntityFactory.makeOperationType("opname", null); + when(val.getLwData()).thenReturn(lwData); + when(val.getMedium()).thenReturn(medium); + when(val.getBioState()).thenReturn(bs); + when(val.getWork()).thenReturn(work); + when(val.getOpType()).thenReturn(opType); + } + + if (valid) { + assertSame(opres, service.performInsideTransaction(user, request)); } else { - med1 = new Medium(101, newMedium); - } - Donor donor = EntityFactory.getDonor(); - SpatialLocation sl = EntityFactory.getSpatialLocation(); - Tissue tissue0 = EntityFactory.makeTissue(donor, sl); - tissue0.setMedium(med0); - tissue0.setReplicate(oldRep); - Tissue tissue1 = service.getOrCreateTissue(tissue0, newRep, med1); - if (!save) { - verifyNoInteractions(mockTissueRepo); - assertSame(tissue0, tissue1); + Matchers.assertValidationException(() -> service.performInsideTransaction(user, request), problems); + } + InOrder inOrder = inOrder(val, mockBlockMakerFactory); + inOrder.verify(val).validate(); + inOrder.verify(val).raiseError(); + if (valid) { + inOrder.verify(mockBlockMakerFactory).createBlockMaker(request, lwData, medium, bs, work, opType, user); + verify(maker).record(); } else { - verify(mockTissueRepo).save(tissue1); - assertNotNull(tissue1.getId()); + verifyNoInteractions(mockBlockMakerFactory); } - assertSame(coalesce(med1, med0), tissue1.getMedium()); - assertEquals(newRep, tissue1.getReplicate()); - } - - @Test - public void testCreateSample() { - Tissue originalTissue = EntityFactory.getTissue(); - BioState bs1 = new BioState(1, "Banana"); - BioState bs2 = new BioState(2, "Custard"); - Sample originalSample = new Sample(5, null, originalTissue, bs1); - Tissue newTissue = originalTissue.derived(); - Labware lw = EntityFactory.makeLabware(EntityFactory.getTubeType(), originalSample); - when(mockSampleRepo.save(any())).then(invocation -> { - Sample sample = invocation.getArgument(0); - assertNull(sample.getId()); - sample.setId(600); - return sample; - }); - Medium medium = new Medium(100, "Oct"); - String repl = "1a"; - doReturn(newTissue).when(service).getOrCreateTissue(any(), any(), any()); - TissueBlockLabware block = new TissueBlockLabware(); - block.setReplicate(repl); - Sample sample = service.createSample(block, lw, bs2, medium); - verify(mockSampleRepo).save(any()); - verify(service).getOrCreateTissue(originalTissue, repl, medium); - assertEquals(600, sample.getId()); - assertSame(bs2, sample.getBioState()); - assertSame(newTissue, sample.getTissue()); - } - - @Test - public void testCreateDestinations() { - Sample sam1 = EntityFactory.getSample(); - Sample sam2 = new Sample(sam1.getId()+1, null, sam1.getTissue(), sam1.getBioState()); - LabwareType lt1 = EntityFactory.makeLabwareType(1,1, "lt1"); - LabwareType lt2 = EntityFactory.makeLabwareType(1,1, "lt2"); - Labware lw1 = EntityFactory.makeLabware(lt1, sam1); - Labware lw2 = EntityFactory.makeLabware(lt2, sam2); - doReturn(lw1, lw2).when(service).createDestination(any(), any(), any()); - TissueBlockLabware block1 = new TissueBlockLabware(); - block1.setLabwareType("lt1"); - block1.setPreBarcode("ABC123"); - TissueBlockLabware block2 = new TissueBlockLabware(); - block2.setLabwareType("lt2"); - - TissueBlockRequest request = new TissueBlockRequest(List.of(block1, block2)); - - List labware = service.createDestinations(request, List.of(sam1, sam2), UCMap.from(LabwareType::getName, lt1, lt2)); - - assertThat(labware).containsExactly(lw1, lw2); - verify(service, times(2)).createDestination(any(), any(), any()); - verify(service).createDestination(lt1, sam1, "ABC123"); - verify(service).createDestination(lt2, sam2, null); - } - - @ParameterizedTest - @ValueSource(booleans={false,true}) - public void testCreateDestination(boolean prebarcoded) { - LabwareType lt = EntityFactory.getTubeType(); - Sample sample = EntityFactory.getSample(); - String prebarcode = prebarcoded ? "ABC123" : null; - Labware lw = EntityFactory.makeEmptyLabware(lt); - when(mockLwService.create(any(), any(), any())).thenReturn(lw); - - assertSame(lw, service.createDestination(lt, sample, prebarcode)); - assertThat(lw.getFirstSlot().getSamples()).containsExactly(sample); - assertTrue(lw.getFirstSlot().isBlock()); - verify(mockLwService).create(lt, prebarcode, prebarcode); - verify(mockSlotRepo).save(lw.getFirstSlot()); - } - - @Test - public void testCreateOperations() { - Operation op1 = new Operation(); - op1.setId(101); - Operation op2 = new Operation(); - op2.setId(102); - OperationType opType = EntityFactory.makeOperationType("Block processing", null); - when(mockOpTypeRepo.getByName("Block processing")).thenReturn(opType); - User user = EntityFactory.getUser(); - LabwareType lt = EntityFactory.getTubeType(); - Labware[] labware = IntStream.range(1, 5) - .mapToObj(i -> { - Labware lw = EntityFactory.makeEmptyLabware(lt); - lw.setId(i); - lw.setBarcode("STAN-"+i); - return lw; - }).toArray(Labware[]::new); - Comment comment = new Comment(5, "Nice one", "sarcasm"); - Map commentMap = new HashMap<>(1); - commentMap.put(comment.getId(), comment); - - TissueBlockLabware block1 = new TissueBlockLabware(); - block1.setSourceBarcode("STAN-1"); - block1.setCommentId(comment.getId()); - TissueBlockLabware block2 = new TissueBlockLabware(); - block2.setSourceBarcode("STAN-2"); - TissueBlockRequest request = new TissueBlockRequest(List.of(block1, block2)); - - doReturn(op1, op2).when(service).createOperation(any(), any(), any(), any(), any()); - - assertThat(service.createOperations(request, user, UCMap.from(Labware::getBarcode, labware[0], labware[1]), - List.of(labware[2], labware[3]), commentMap)).containsExactly(op1, op2); - - verify(service, times(2)).createOperation(any(), any(), any(), any(), any()); - verify(service).createOperation(opType, user, labware[0], labware[2], comment); - verify(service).createOperation(opType, user, labware[1], labware[3], null); - } - - @ParameterizedTest - @ValueSource(booleans={false,true}) - public void testCreateOperation(boolean hasComment) { - Comment comment = (hasComment ? new Comment(5, "Yeah right", "sarcasm") : null); - User user = EntityFactory.getUser(); - OperationType opType = EntityFactory.makeOperationType("Block processing", null); - - final Sample sam0 = EntityFactory.getSample(); - final Sample sam1 = new Sample(sam0.getId()+1, null, sam0.getTissue(), sam0.getBioState()); - Labware lw0 = EntityFactory.makeLabware(EntityFactory.getTubeType(), sam0); - Labware lw1 = EntityFactory.makeLabware(EntityFactory.getTubeType(), sam1); - - Operation op = new Operation(); - op.setId(500); - when(mockOpService.createOperation(any(), any(), any(), any())).thenReturn(op); - - assertSame(op, service.createOperation(opType, user, lw0, lw1, comment)); - verify(mockOpService).createOperation(opType, user, List.of( - new Action(null, null, lw0.getFirstSlot(), lw1.getFirstSlot(), sam1, sam0)), - null); - if (comment==null) { - verifyNoInteractions(mockOpCommentRepo); - } else { - verify(mockOpCommentRepo).save(new OperationComment(null, comment, 500, sam1.getId(), lw1.getFirstSlot().getId(), null)); - } - } - - @Test - public void testDiscardSources_none() { - service.discardSources(List.of(), new UCMap<>()); - } - - @Test - public void testDiscardSources() { - LabwareType lt = EntityFactory.getTubeType(); - Labware lw1 = EntityFactory.makeEmptyLabware(lt); - Labware lw2 = EntityFactory.makeEmptyLabware(lt); - Labware lw3 = EntityFactory.makeEmptyLabware(lt); - service.discardSources(List.of(lw1.getBarcode(), lw2.getBarcode()), UCMap.from(Labware::getBarcode, lw1, lw2, lw3)); - verify(mockLwRepo).saveAll(List.of(lw1, lw2)); - assertTrue(lw1.isDiscarded()); - assertTrue(lw2.isDiscarded()); - assertFalse(lw3.isDiscarded()); } } \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestLabwareValidator.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestLabwareValidator.java index ff3c84923..531ca0135 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestLabwareValidator.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestLabwareValidator.java @@ -268,10 +268,10 @@ public void testValidateSingleSample() { public void testValidateBlock() { final Runnable action = validator::validateBlock; testValidator(List.of(), action); - Sample sam1 = EntityFactory.getSample(); - Sample sam2 = new Sample(sam1.getId()+1, "800", sam1.getTissue(), sam1.getBioState()); - Labware goodLw = EntityFactory.makeBlock(sam1); - Labware badLw = EntityFactory.makeLabware(EntityFactory.getTubeType(), sam2); + Sample[] samples = EntityFactory.makeSamples(2); + samples[0].setBlockHighestSection(1); + Labware goodLw = EntityFactory.makeTube(samples[0]); + Labware badLw = EntityFactory.makeTube(samples[1]); testValidator(List.of(goodLw), action); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestParaffinProcessingService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestParaffinProcessingService.java index 93117e378..4863961fb 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestParaffinProcessingService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestParaffinProcessingService.java @@ -11,6 +11,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.OperationResult; import uk.ac.sanger.sccp.stan.request.ParaffinProcessingRequest; +import uk.ac.sanger.sccp.stan.service.ParaffinProcessingServiceImp.BlockChange; import uk.ac.sanger.sccp.stan.service.work.WorkService; import java.util.*; @@ -38,6 +39,8 @@ public class TestParaffinProcessingService { @Mock private TissueRepo mockTissueRepo; @Mock + private SampleRepo mockSampleRepo; + @Mock private SlotRepo mockSlotRepo; @Mock private WorkService mockWorkService; @@ -225,11 +228,12 @@ public void testRecord() { doReturn(ops).when(service).createOps(any(), any()); doNothing().when(service).recordComments(any(), any()); doNothing().when(service).updateMedium(any(), any()); - doNothing().when(service).createBlocks(any()); + List changes = List.of(new BlockChange(null, null, null)); + doReturn(changes).when(service).createBlocks(any()); assertEquals(new OperationResult(ops, labware), service.record(user, labware, work, comment, medium)); verify(service).updateMedium(labware, medium); - verify(service).createOps(user, labware); + verify(service).createOps(user, changes); verify(mockWorkService).link(work, ops); verify(service).recordComments(comment, ops); verify(service).createBlocks(labware); @@ -282,17 +286,22 @@ public void testCreateOps() { when(mockOpTypeRepo.getByName(opType.getName())).thenReturn(opType); User user = EntityFactory.getUser(); LabwareType lt = EntityFactory.getTubeType(); - Sample sample = EntityFactory.getSample(); + Sample[] samples = EntityFactory.makeSamples(4); List labware = IntStream.range(0, 2) - .mapToObj(i -> EntityFactory.makeLabware(lt, sample)) - .collect(toList()); + .mapToObj(i -> EntityFactory.makeLabware(lt, samples[i*2])) + .toList(); List ops = makeOps(2); - when(mockOpService.createOperationInPlace(any(), any(), any(), any(), any())).thenReturn(ops.get(0), ops.get(1)); - - assertThat(service.createOps(user, labware)).containsExactlyElementsOf(ops); + when(mockOpService.createOperation(any(), any(), any(), any())).thenReturn(ops.get(0), ops.get(1)); + List changes = IntStream.range(0, samples.length/2) + .mapToObj(i -> new BlockChange(labware.get(i).getFirstSlot(), samples[2*i], samples[2*i+1])) + .toList(); + assertThat(service.createOps(user, changes)).containsExactlyElementsOf(ops); - for (Labware lw : labware) { - verify(mockOpService).createOperationInPlace(opType, user, lw, null, null); + verify(mockOpService, times(labware.size())).createOperation(any(), any(), any(), any()); + for (int i = 0; i < labware.size(); ++i) { + Slot slot = labware.get(i).getFirstSlot(); + Action ac = new Action(null, null, slot, slot, samples[2*i+1], samples[2*i]); + verify(mockOpService).createOperation(opType, user, List.of(ac), null); } } @@ -304,32 +313,45 @@ public void testCreateBlocks(boolean anyChanged) { Sample[] samples = IntStream.rangeClosed(101,104) .mapToObj(i -> new Sample(i, null, tissue, bs)) .toArray(Sample[]::new); - LabwareType lt = EntityFactory.getTubeType(); - List labware; if (anyChanged) { - labware = IntStream.range(0, 4).mapToObj( - i -> (i < 2 ? EntityFactory.makeBlock(samples[i]) : EntityFactory.makeLabware(lt, samples[i])) - ).toList(); + samples[0].setBlockHighestSection(2); + samples[1].setBlockHighestSection(2); } else { - labware = IntStream.range(0,4).mapToObj( - i -> EntityFactory.makeBlock(samples[i]) - ).toList(); + for (Sample sam : samples) { + sam.setBlockHighestSection(2); + } } + List labware = Arrays.stream(samples).map(EntityFactory::makeTube).toList(); List changedSlots; + List changedSamples; if (anyChanged) { changedSlots = labware.subList(2,4).stream().map(Labware::getFirstSlot).toList(); + changedSamples = Stream.of(samples[2], samples[3]).map(sam -> Sample.newBlock(null, sam.getTissue(), sam.getBioState(), 0)).toList(); } else { - changedSlots = List.of(); + changedSlots = null; + changedSamples = null; } - service.createBlocks(labware); + List changes = service.createBlocks(labware); if (anyChanged) { verify(mockSlotRepo).saveAll(changedSlots); + verify(mockSampleRepo).saveAll(changedSamples); } else { verifyNoInteractions(mockSlotRepo); + verifyNoInteractions(mockSampleRepo); } for (Labware lw : labware) { assertTrue(lw.getFirstSlot().isBlock()); } + BlockChange[] expectedChanges = IntStream.range(0, samples.length) + .mapToObj(i -> new BlockChange(labware.get(i).getFirstSlot(), samples[i], samples[i])) + .toArray(BlockChange[]::new); + + if (anyChanged) { + for (int i = 2; i < 4; ++i) { + expectedChanges[i] = new BlockChange(labware.get(i).getFirstSlot(), samples[i], changedSamples.get(i-2)); + } + } + assertThat(changes).containsExactlyInAnyOrder(expectedChanges); } @Test diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestSampleService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestSampleService.java deleted file mode 100644 index 40cccf409..000000000 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestSampleService.java +++ /dev/null @@ -1,73 +0,0 @@ -package uk.ac.sanger.sccp.stan.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.ac.sanger.sccp.stan.EntityFactory; -import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.repo.PlanActionRepo; -import uk.ac.sanger.sccp.stan.repo.SlotRepo; - -import java.util.*; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -/** - * Tests for {@link SampleService} - * @author dr6 - */ -public class TestSampleService { - private PlanActionRepo mockPlanActionRepo; - private SlotRepo mockSlotRepo; - - private SampleService sampleService; - - @BeforeEach - void setup() { - mockPlanActionRepo = mock(PlanActionRepo.class); - mockSlotRepo = mock(SlotRepo.class); - - sampleService = new SampleService(mockPlanActionRepo, mockSlotRepo); - } - - @ParameterizedTest - @MethodSource("nextSectionData") - public void testNextSection(Integer blockMaxSection, Integer planMaxSection, - int expectedNextSection) { - Sample sample = EntityFactory.getSample(); - int slotId = 100; - Slot slot = new Slot(slotId, 10, new Address(1,1), - List.of(sample), sample.getId(), blockMaxSection); - when(mockPlanActionRepo.findMaxPlannedSectionFromSlotId(slotId)) - .thenReturn(toOptional(planMaxSection)); - - assertEquals(sampleService.nextSection(slot), expectedNextSection); - verify(mockSlotRepo).save(slot); - assertEquals(slot.getBlockSampleId(), sample.getId()); - assertEquals(slot.getBlockHighestSection(), expectedNextSection); - } - - static Stream nextSectionData() { - return Arrays.stream(new Integer[][] { - {0, null, 1}, - {0, 0, 1}, - {4, null, 5}, - {4, 0, 5}, - {4, 2, 5}, - {0, null, 1}, - {0, 0, 1}, - {1, 2, 3}, - {0, 4, 5}, - {0, 4, 5}, - {2, 4, 5}, - {4, 4, 5}, - }).map(Arguments::of); - } - - private static OptionalInt toOptional(Integer num) { - return (num==null ? OptionalInt.empty() : OptionalInt.of(num)); - } -} diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestUnreleaseService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestUnreleaseService.java index 4bc3987c6..d638646ed 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestUnreleaseService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestUnreleaseService.java @@ -32,7 +32,7 @@ public class TestUnreleaseService { private LabwareValidatorFactory mockLabwareValidatorFactory; private LabwareRepo mockLwRepo; - private SlotRepo mockSlotRepo; + private SampleRepo mockSampleRepo; private OperationTypeRepo mockOpTypeRepo; private OperationService mockOpService; @@ -43,12 +43,12 @@ public class TestUnreleaseService { void setup() { mockLabwareValidatorFactory = mock(LabwareValidatorFactory.class); mockLwRepo = mock(LabwareRepo.class); - mockSlotRepo = mock(SlotRepo.class); + mockSampleRepo = mock(SampleRepo.class); mockOpTypeRepo = mock(OperationTypeRepo.class); mockOpService = mock(OperationService.class); mockWorkService = mock(WorkService.class); - service = spy(new UnreleaseServiceImp(mockLabwareValidatorFactory, mockLwRepo, mockSlotRepo, + service = spy(new UnreleaseServiceImp(mockLabwareValidatorFactory, mockLwRepo, mockSampleRepo, mockOpTypeRepo, mockOpService, mockWorkService)); } @@ -199,19 +199,18 @@ public void testValidateRequest() { @ParameterizedTest @CsvSource(value={ - "true, 2, 5,", - "true, 4, 3, 'For block STAN-1, cannot reduce the highest section number from 4 to 3.'", - "true,,2,", - "true,,-2,Cannot set the highest section to a negative number.", - "false,,1,Cannot set the highest section number from labware STAN-1 because it is not a block.", + "2, 5,", + "4, 3, 'For block STAN-1, cannot reduce the highest section number from 4 to 3.'", + "1,-2,Cannot set the highest section to a negative number.", + ",1,Cannot set the highest section number from labware STAN-1 because it is not a block.", }) - public void testHighestSectionProblem(boolean isBlock, Integer oldNum, int num, String expectedProblem) { - Sample sample = EntityFactory.getSample(); - Labware lw = (isBlock ? EntityFactory.makeBlock(sample) : EntityFactory.makeLabware(EntityFactory.getTubeType(), sample)); - lw.setBarcode("STAN-1"); - if (oldNum!=null || isBlock) { - lw.getFirstSlot().setBlockHighestSection(oldNum); + public void testHighestSectionProblem(Integer oldNum, int num, String expectedProblem) { + Sample sample = EntityFactory.makeSamples(1)[0]; + if (oldNum != null) { + sample.setBlockHighestSection(oldNum); } + Labware lw = EntityFactory.makeTube(sample); + lw.setBarcode("STAN-1"); assertEquals(expectedProblem, service.highestSectionProblem(lw, num)); } @@ -240,12 +239,12 @@ public void testPerform() { @Test public void testUpdateLabware() { - final List slotUpdates = new ArrayList<>(); + final List sampleUpdates = new ArrayList<>(); final List lwUpdates = new ArrayList<>(); - when(mockSlotRepo.saveAll(any())).then(invocation -> { - List slots = invocation.getArgument(0); - slotUpdates.addAll(slots); - return slots; + when(mockSampleRepo.saveAll(any())).then(invocation -> { + List samples = invocation.getArgument(0); + sampleUpdates.addAll(samples); + return samples; }); when(mockLwRepo.saveAll(any())).then(invocation -> { List lw = invocation.getArgument(0); @@ -253,19 +252,15 @@ public void testUpdateLabware() { return lw; }); - Sample sample = EntityFactory.getSample(); LabwareType lt = EntityFactory.getTubeType(); + Sample[] samples = EntityFactory.makeSamples(5); Labware[] labware = IntStream.range(0,5).mapToObj(i -> { - Labware lw = EntityFactory.makeLabware(lt, sample); + if (i > 0) { + samples[i].setBlockHighestSection(i); + } + Labware lw = EntityFactory.makeLabware(lt, samples[i]); lw.setBarcode("STAN-" + i); lw.setReleased(true); - if (i>0) { - final Slot slot = lw.getFirstSlot(); - slot.setBlockSampleId(sample.getId()); - if (i < 4) { - slot.setBlockHighestSection(i); - } - } return lw; }).toArray(Labware[]::new); @@ -288,10 +283,10 @@ public void testUpdateLabware() { } Integer[] expectedHighestSection = { null, 5, 10, 3, 12 }; for (int i = 0; i < labware.length; ++i) { - assertEquals(expectedHighestSection[i], labware[i].getFirstSlot().getBlockHighestSection()); + assertEquals(expectedHighestSection[i], labware[i].getFirstSlot().getSamples().getFirst().getBlockHighestSection()); } - Slot[] expectedSlotUpdates = IntStream.of(1,2,4).mapToObj(i -> labware[i].getFirstSlot()).toArray(Slot[]::new); - assertThat(slotUpdates).containsExactly(expectedSlotUpdates); + Sample[] expectedSampleUpdates = IntStream.of(1,2,4).mapToObj(i -> samples[i]).toArray(Sample[]::new); + assertThat(sampleUpdates).containsExactlyInAnyOrder(expectedSampleUpdates); } @Test diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/block/TestBlockMaker.java b/src/test/java/uk/ac/sanger/sccp/stan/service/block/TestBlockMaker.java new file mode 100644 index 000000000..ba4d2bd51 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/block/TestBlockMaker.java @@ -0,0 +1,330 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.*; +import uk.ac.sanger.sccp.stan.EntityFactory; +import uk.ac.sanger.sccp.stan.Matchers; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.OperationResult; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockContent; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockLabware; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.Zip; + +import java.util.*; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** Test {@link BlockMakerImp} */ +class TestBlockMaker { + @Mock + TissueRepo mockTissueRepo; + @Mock + SampleRepo mockSampleRepo; + @Mock + SlotRepo mockSlotRepo; + @Mock + LabwareRepo mockLwRepo; + @Mock + OperationCommentRepo mockOpcomRepo; + @Mock + LabwareService mockLwService; + @Mock + OperationService mockOpService; + @Mock + WorkService mockWorkService; + @Mock + BioRiskService mockBioRiskService; + + private AutoCloseable mocking; + + @BeforeEach + void setup() { + mocking = MockitoAnnotations.openMocks(this); + } + + @AfterEach + void cleanup() throws Exception { + mocking.close(); + } + + private BlockMakerImp makeBlockMaker(TissueBlockRequest request, List blds, + Medium medium, BioState bs, Work work, OperationType opType, User user) { + return new BlockMakerImp(mockTissueRepo, mockSampleRepo, mockSlotRepo, mockLwRepo, mockOpcomRepo, + mockLwService, mockOpService, mockWorkService, mockBioRiskService, + request, blds, medium, bs, work, opType, user); + } + + @Test + void testRecord() { + Work work = EntityFactory.makeWork("SGP1"); + BlockMakerImp maker = spy(makeBlockMaker(null, null, null, null, work, null, null)); + List lws = List.of(EntityFactory.getTube()); + Operation op = new Operation(); + op.setId(100); + List ops = List.of(op); + doReturn(lws).when(maker).createLabware(); + doNothing().when(maker).createSamples(); + doNothing().when(maker).findSlots(); + doNothing().when(maker).fillLabware(); + doNothing().when(maker).discardSources(); + doReturn(ops).when(maker).createOperations(); + OperationResult opres = maker.record(); + + verify(maker).createLabware(); + verify(maker).createSamples(); + verify(maker).findSlots(); + verify(maker).fillLabware(); + verify(maker).createOperations(); + verify(mockWorkService).link(work, ops); + verify(mockBioRiskService).copyOpSampleBioRisks(ops); + verify(maker).discardSources(); + + assertEquals(lws, opres.getLabware()); + assertEquals(ops, opres.getOperations()); + } + + @Test + void testCreateLabware() { + LabwareType[] lts = { EntityFactory.getTubeType(), + EntityFactory.makeLabwareType(1, 1)}; + List lws = Arrays.stream(lts).map(EntityFactory::makeEmptyLabware).toList(); + List tbls = List.of(new TissueBlockLabware(), new TissueBlockLabware()); + final String prebarcode = "PREB1"; + tbls.get(0).setPreBarcode(prebarcode); + List lds = tbls.stream().map(BlockLabwareData::new).toList(); + Zip.of(Arrays.stream(lts), lds.stream()).forEach((lt, ld) -> ld.setLwType(lt)); + Zip.of(Arrays.stream(lts), lws.stream()).forEach((lt, lw) -> when(mockLwService.create(same(lt), any(), any())).thenReturn(lw)); + BlockMakerImp maker = makeBlockMaker(new TissueBlockRequest(tbls), lds, null, null, null, null, null); + List result = maker.createLabware(); + verify(mockLwService).create(lts[0], prebarcode, prebarcode); + verify(mockLwService).create(lts[1], null, null); + assertEquals(lws, result); + } + + @ParameterizedTest + @CsvSource({ + "rep1,rep1, MED,MED, false", + ",rep1, MED,MED, true", + "rep1,rep1, WAT,MED, true", + }) + void testGetOrCreateTissue(String oldRep, String newRep, String oldMedium, String newMedium, boolean expectNew) { + Medium oldMed = new Medium(1, oldMedium); + Medium newMed = oldMedium.equalsIgnoreCase(newMedium) ? oldMed : new Medium(2, newMedium); + Donor donor = EntityFactory.getDonor(); + SpatialLocation sl = EntityFactory.getSpatialLocation(); + Tissue oldTissue = EntityFactory.makeTissue(donor, sl); + oldTissue.setReplicate(oldRep); + oldTissue.setMedium(oldMed); + when(mockTissueRepo.save(any())).then(Matchers.returnArgument()); + + BlockMakerImp maker = makeBlockMaker(null, null, null, null, null, null, null); + Tissue newTissue = maker.getOrCreateTissue(oldTissue, newRep, newMed); + if (expectNew) { + verify(mockTissueRepo).save(newTissue); + assertNotEquals(oldTissue, newTissue); + assertNull(newTissue.getId()); + assertEquals(newRep, newTissue.getReplicate()); + assertEquals(newMed, newTissue.getMedium()); + } else { + verifyNoInteractions(mockTissueRepo); + assertSame(oldTissue, newTissue); + } + } + + @Test + void testCreateSamples() { + BioState bs = EntityFactory.getBioState(); + Medium medium = EntityFactory.getMedium(); + Sample[] sourceSamples = EntityFactory.makeSamples(2); + Tissue altTissue = EntityFactory.makeTissue(EntityFactory.getDonor(), EntityFactory.getSpatialLocation()); + String[] reps = {"rep1", "rep2"}; + List bds = IntStream.range(0, 2).mapToObj(i -> new BlockData(new TissueBlockContent())).toList(); + for (int i = 0; i < sourceSamples.length; ++i) { + bds.get(i).setSourceSample(sourceSamples[i]); + bds.get(i).getRequestContent().setReplicate(reps[i]); + } + List bls = List.of(new BlockLabwareData(new TissueBlockLabware())); + bls.getFirst().setBlocks(bds); + BlockMakerImp maker = spy(makeBlockMaker(null, bls, medium, bs, null, null, null)); + doAnswer(Matchers.returnArgument()).when(maker).getOrCreateTissue(any(), eq(reps[0]), any()); + doReturn(altTissue).when(maker).getOrCreateTissue(any(), eq(reps[1]), any()); + when(mockSampleRepo.save(any())).then(Matchers.returnArgument()); + + maker.createSamples(); + Sample[] newSamples = bds.stream().map(BlockData::getSample).toArray(Sample[]::new); + for (int i = 0; i < newSamples.length; i++) { + verify(maker).getOrCreateTissue(same(sourceSamples[i].getTissue()), eq(reps[i]), same(medium)); + Sample sam = newSamples[i]; + assertNull(sam.getId()); + verify(mockSampleRepo).save(sam); + assertSame(i==0 ? sourceSamples[0].getTissue() : altTissue, sam.getTissue()); + assertEquals(0, sam.getBlockHighestSection()); + assertSame(bs, sam.getBioState()); + assertSame(medium, sam.getTissue().getMedium()); + } + } + + @Test + void testFindSlots() { + LabwareType lt = EntityFactory.makeLabwareType(1, 3); + Sample[] samples = EntityFactory.makeSamples(2); + Labware sourceLw = EntityFactory.makeEmptyLabware(lt); + List sourceSlots = sourceLw.getSlots(); + sourceSlots.get(0).addSample(samples[0]); + sourceSlots.get(1).addSample(samples[1]); + sourceSlots.get(2).addSample(samples[1]); + sourceSlots.get(2).addSample(samples[0]); + Labware destLw = EntityFactory.makeEmptyLabware(lt); + TissueBlockContent con = new TissueBlockContent(); + Address A1 = new Address(1,1), A2 = new Address(1,2); + con.setAddresses(List.of(A1, A2)); + BlockData bd = new BlockData(con); + bd.setSourceLabware(sourceLw); + bd.setSourceSample(samples[0]); + BlockLabwareData bl = new BlockLabwareData(new TissueBlockLabware()); + bl.setLabware(destLw); + bl.setBlocks(List.of(bd)); + BlockMakerImp maker = makeBlockMaker(null, List.of(bl), null, null, null, null, null); + maker.findSlots(); + assertThat(bd.getSourceSlots()).containsExactlyInAnyOrder(sourceSlots.get(0), sourceSlots.get(2)); + assertThat(bd.getDestSlots()).containsExactlyInAnyOrder(destLw.getSlot(A1), destLw.getSlot(A2)); + } + + @Test + void testCreateOperations() { + List bls = IntStream.range(0,2).mapToObj(i -> new BlockLabwareData(new TissueBlockLabware())).toList(); + Operation[] ops = IntStream.range(10,12).mapToObj(i -> { + Operation op = new Operation(); + op.setId(i); + return op; + }).toArray(Operation[]::new); + BlockMakerImp maker = spy(makeBlockMaker(null, bls, null, null, null, null, null)); + doReturn(ops[0], ops[1]).when(maker).createOperation(any()); + doNothing().when(maker).recordComments(any(), any()); + assertThat(maker.createOperations()).containsExactly(ops); + bls.forEach(bl -> verify(maker).createOperation(same(bl))); + Zip.of(bls.stream(), Arrays.stream(ops)).forEach((bl, op) -> verify(maker).recordComments(bl, op.getId())); + } + + @Test + void testCreateOperation() { + OperationType opType = EntityFactory.makeOperationType("opname", null); + User user = EntityFactory.getUser(); + final Address A1 = new Address(1,1), A2 = new Address(1,2); + Sample[] samples = EntityFactory.makeSamples(4); + LabwareType lt = EntityFactory.makeLabwareType(1,2); + Labware[] lws = IntStream.range(0,3).mapToObj(i -> EntityFactory.makeEmptyLabware(lt)).toArray(Labware[]::new); + BlockData bd1 = new BlockData(new TissueBlockContent()); + bd1.setSourceSample(samples[0]); + bd1.setSourceSlots(List.of(lws[0].getSlot(A1), lws[0].getSlot(A2))); + bd1.setDestSlots(List.of(lws[2].getSlot(A1))); + bd1.setSample(samples[2]); + BlockData bd2 = new BlockData(new TissueBlockContent()); + bd2.setSourceSample(samples[1]); + bd2.setSample(samples[3]); + bd2.setSourceSlots(List.of(lws[1].getSlot(A1))); + bd2.setDestSlots(List.of(lws[2].getSlot(A1), lws[2].getSlot(A2))); + BlockLabwareData ld = new BlockLabwareData(new TissueBlockLabware()); + ld.setBlocks(List.of(bd1, bd2)); + + Operation op = new Operation(); + op.setId(100); + when(mockOpService.createOperation(any(), any(), any(), any())).thenReturn(op); + BlockMakerImp maker = makeBlockMaker(null, List.of(ld), null, null, null, opType, user); + assertThat(maker.createOperations()).containsExactly(op); + ArgumentCaptor> acCaptor = Matchers.genericCaptor(List.class); + verify(mockOpService).createOperation(same(opType), same(user), acCaptor.capture(), isNull()); + List actions = acCaptor.getValue(); + assertThat(actions).containsExactlyInAnyOrder( + new Action(null, null, lws[0].getSlot(A1), lws[2].getSlot(A1), samples[2], samples[0]), + new Action(null, null, lws[0].getSlot(A2), lws[2].getSlot(A1), samples[2], samples[0]), + new Action(null, null, lws[1].getSlot(A1), lws[2].getSlot(A1), samples[3], samples[1]), + new Action(null, null, lws[1].getSlot(A1), lws[2].getSlot(A2), samples[3], samples[1]) + ); + } + + @Test + void testRecordComments() { + final Integer opId = 100; + + Labware lw = EntityFactory.makeEmptyLabware(EntityFactory.makeLabwareType(1, 2)); + BlockData bd = new BlockData(new TissueBlockContent()); + Comment com = new Comment(1, "com1", "cat"); + bd.setComment(com); + Sample sam = EntityFactory.getSample(); + bd.setSample(sam); + bd.setDestSlots(lw.getSlots()); + BlockData bd0 = new BlockData(new TissueBlockContent()); + bd0.setComment(null); + BlockLabwareData ld = new BlockLabwareData(new TissueBlockLabware()); + ld.setBlocks(List.of(bd0, bd)); + + BlockMakerImp maker = makeBlockMaker(null, List.of(ld), null, null, null, null, null); + maker.recordComments(ld, opId); + ArgumentCaptor> comCaptor = Matchers.genericCaptor(List.class); + verify(mockOpcomRepo).saveAll(comCaptor.capture()); + List opcoms = comCaptor.getValue(); + assertThat(opcoms).hasSize(2); + for (OperationComment opcom : opcoms) { + assertNull(opcom.getId()); + assertSame(com, opcom.getComment()); + assertEquals(opId, opcom.getOperationId()); + assertSame(sam.getId(), opcom.getSampleId()); + assertNull(opcom.getLabwareId()); + } + Integer[] slotIds = lw.getSlots().stream().map(Slot::getId).toArray(Integer[]::new); + assertThat(opcoms.stream().map(OperationComment::getSlotId)).containsExactlyInAnyOrder(slotIds); + } + + @Test + void testFillLabware() { + final Address A1 = new Address(1,1), A2 = new Address(1,2), A3 = new Address(1,3); + LabwareType lt = EntityFactory.makeLabwareType(1,4); + Labware lw = EntityFactory.makeEmptyLabware(lt); + Sample[] samples = EntityFactory.makeSamples(2); + List bds = IntStream.range(0,2).mapToObj(i -> { + BlockData bd = new BlockData(new TissueBlockContent()); + bd.setSample(samples[i]); + bd.setDestSlots(i==0 ? List.of(lw.getSlot(A1)) : List.of(lw.getSlot(A2), lw.getSlot(A3))); + return bd; + }).toList(); + BlockLabwareData bl = new BlockLabwareData(new TissueBlockLabware()); + bl.setBlocks(bds); + BlockMakerImp maker = makeBlockMaker(null, List.of(bl), null, null, null, null, null); + maker.fillLabware(); + assertThat(lw.getSlot(A1).getSamples()).containsExactly(samples[0]); + assertThat(lw.getSlot(A2).getSamples()).containsExactly(samples[1]); + assertThat(lw.getSlot(A3).getSamples()).containsExactly(samples[1]); + verify(mockSlotRepo).saveAll(Set.of(lw.getSlot(A1), lw.getSlot(A2), lw.getSlot(A3))); + } + + @Test + void testDiscardSources() { + TissueBlockRequest request = new TissueBlockRequest(); + LabwareType lt = EntityFactory.getTubeType(); + List lws = IntStream.range(0,4).mapToObj(i -> EntityFactory.makeEmptyLabware(lt)).toList(); + List bds = lws.stream().map(lw -> { + BlockData bd = new BlockData(new TissueBlockContent()); + bd.setSourceLabware(lw); + return bd; + }).toList(); + BlockLabwareData ld = new BlockLabwareData(new TissueBlockLabware()); + ld.setBlocks(bds); + lws.get(1).setDiscarded(true); + request.setDiscardSourceBarcodes(lws.subList(0,3).stream().map(Labware::getBarcode).toList()); + BlockMakerImp maker = makeBlockMaker(request, List.of(ld), null, null, null, null, null); + maker.discardSources(); + + Zip.enumerate(lws.stream()).forEach((i, lw) -> assertEquals(i != 3, lw.isDiscarded(), ""+i)); + verify(mockLwRepo).saveAll(Set.of(lws.get(0), lws.get(2))); + } +} \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/block/TestBlockValidator.java b/src/test/java/uk/ac/sanger/sccp/stan/service/block/TestBlockValidator.java new file mode 100644 index 000000000..6af2f10a5 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/block/TestBlockValidator.java @@ -0,0 +1,500 @@ +package uk.ac.sanger.sccp.stan.service.block; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.*; +import org.mockito.stubbing.Answer; +import uk.ac.sanger.sccp.stan.EntityFactory; +import uk.ac.sanger.sccp.stan.Matchers; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockContent; +import uk.ac.sanger.sccp.stan.request.TissueBlockRequest.TissueBlockLabware; +import uk.ac.sanger.sccp.stan.service.*; +import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.Zip; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.*; +import static uk.ac.sanger.sccp.stan.Matchers.*; + +/** Tests {@link BlockValidatorImp} */ +class TestBlockValidator { + @Mock + LabwareValidatorFactory mockLwValFactory; + @Mock + Validator mockPrebarcodeValidator; + @Mock + Validator mockReplicateValidator; + @Mock + LabwareRepo mockLwRepo; + @Mock + OperationTypeRepo mockOpTypeRepo; + @Mock + LabwareTypeRepo mockLtRepo; + @Mock + BioStateRepo mockBsRepo; + @Mock + TissueRepo mockTissueRepo; + @Mock + MediumRepo mockMediumRepo; + @Mock + CommentValidationService mockCommentValidationService; + @Mock + WorkService mockWorkService; + + private AutoCloseable mocking; + + @BeforeEach + void setup() { + mocking = MockitoAnnotations.openMocks(this); + } + + @AfterEach + void cleanup() throws Exception { + mocking.close(); + } + + BlockValidatorImp makeVal(TissueBlockRequest request) { + return new BlockValidatorImp(mockLwValFactory, mockPrebarcodeValidator, mockReplicateValidator, + mockLwRepo, mockOpTypeRepo, mockLtRepo, mockBsRepo, mockTissueRepo, mockMediumRepo, + mockCommentValidationService, mockWorkService, + request); + } + + @Test + void testValidate_noLabware() { + TissueBlockRequest request = new TissueBlockRequest(); + BlockValidatorImp val = makeVal(request); + val.validate(); + assertProblem(val.getProblems(), "No labware specified."); + } + + @Test + void testValidate() { + TissueBlockRequest request = new TissueBlockRequest(); + TissueBlockLabware rlw = new TissueBlockLabware(); + TissueBlockContent content = new TissueBlockContent(); + content.setAddresses(List.of(new Address(1,1))); + rlw.setContents(List.of(content)); + request.setLabware(List.of(rlw)); + + BlockValidatorImp val = spy(makeVal(request)); + doNothing().when(val).loadEntities(); + doNothing().when(val).checkDestAddresses(); + doNothing().when(val).checkPrebarcodes(); + doNothing().when(val).checkReplicates(); + doAnswer(addProblem(val, "Bad barcode")).when(val).checkDiscardBarcodes(); + + val.validate(); + + InOrder inOrder = inOrder(val); + inOrder.verify(val).loadEntities(); + inOrder.verify(val).checkDestAddresses(); + inOrder.verify(val).checkPrebarcodes(); + inOrder.verify(val).checkReplicates(); + inOrder.verify(val).checkDiscardBarcodes(); + + assertThat(val.getLwData()).containsExactly(new BlockLabwareData(request.getLabware().getFirst())); + assertThat(val.getProblems()).containsExactly("Bad barcode"); + } + + static Answer addProblem(BlockValidatorImp val, String problem, X returnValue) { + return invocation -> { + val.getProblems().add(problem); + return returnValue; + }; + } + + static Answer addProblem(BlockValidatorImp val, String problem) { + return addProblem(val, problem, null); + } + + @Test + void testLoadEntities() { + Work work = EntityFactory.makeWork("SGP1"); + OperationType opType = EntityFactory.makeOperationType("Block processing", null); + BioState bs = EntityFactory.getBioState(); + Medium medium = EntityFactory.getMedium(); + when(mockWorkService.validateUsableWork(any(), any())).thenReturn(work); + when(mockBsRepo.findByName(any())).thenReturn(Optional.of(bs)); + when(mockOpTypeRepo.findByName(any())).thenReturn(Optional.of(opType)); + when(mockMediumRepo.findByName(any())).thenReturn(Optional.of(medium)); + BlockValidatorImp val = spy(makeVal(new TissueBlockRequest())); + doNothing().when(val).loadSources(); + doNothing().when(val).loadSourceSamples(); + doNothing().when(val).loadLabwareTypes(); + doNothing().when(val).loadComments(); + + val.loadEntities(); + verify(val).loadSources(); + verify(val).loadSourceSamples(); + verify(val).loadLabwareTypes(); + verify(val).loadComments(); + assertSame(work, val.getWork()); + assertSame(opType, val.getOpType()); + assertSame(bs, val.getBioState()); + assertSame(medium, val.getMedium()); + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testLoadSources(boolean ok) { + LabwareValidator lwVal = mock(LabwareValidator.class); + BioState bs = EntityFactory.getBioState(); + when(mockLwValFactory.getValidator()).thenReturn(lwVal); + LabwareType lt = EntityFactory.getTubeType(); + Labware[] lws = IntStream.range(0,2).mapToObj(i -> EntityFactory.makeEmptyLabware(lt)).toArray(Labware[]::new); + List cons = Stream.of(ok ? lws[0].getBarcode().toLowerCase() : null, lws[0].getBarcode(), lws[1].getBarcode()) + .map(s -> { + TissueBlockContent c = new TissueBlockContent(); + c.setSourceBarcode(s); + return c; + }).toList(); + List tbls = cons.stream().map(con -> { + TissueBlockLabware tbl = new TissueBlockLabware(); + tbl.setContents(List.of(con)); + return tbl; + }).toList(); + TissueBlockRequest request = new TissueBlockRequest(); + request.setLabware(tbls); + when(lwVal.getLabware()).thenReturn(Arrays.asList(lws)); + if (!ok) { + when(lwVal.getErrors()).thenReturn(List.of("Bad labware")); + } + + BlockValidatorImp val = makeVal(request); + List lds = tbls.stream().map(BlockLabwareData::new).toList(); + val.setLwData(lds); + val.setProblems(new LinkedHashSet<>()); + val.setBioState(bs); + val.loadSources(); + assertSame(ok ? lws[0] : null, lds.get(0).getBlocks().getFirst().getSourceLabware()); + assertSame(lws[0], lds.get(1).getBlocks().getFirst().getSourceLabware()); + assertSame(lws[1], lds.get(2).getBlocks().getFirst().getSourceLabware()); + if (ok) { + assertThat(val.getProblems()).isEmpty(); + } else { + assertThat(val.getProblems()).containsExactlyInAnyOrder("Bad labware", "Source barcode missing."); + } + + verify(lwVal).loadLabware(mockLwRepo, Set.of(lws[0].getBarcode(), lws[1].getBarcode())); + verify(lwVal, never()).setSingleSample(true); + verify(lwVal).validateSources(); + verify(lwVal).validateBioState(bs); + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testLoadSourceSamples(boolean ok) { + Sample[] samples = EntityFactory.makeSamples(2); + LabwareType lt = EntityFactory.makeLabwareType(1,2); + Labware lw1 = EntityFactory.makeLabware(lt, samples); + List cons = List.of(new TissueBlockContent(), new TissueBlockContent()); + Integer badSampleId; + if (ok) { + badSampleId = null; + Zip.of(cons.stream(), Arrays.stream(samples)).forEach((con, sam) -> con.setSourceSampleId(sam.getId())); + } else { + badSampleId = samples[1].getId()+1; + cons.get(0).setSourceSampleId(badSampleId); + } + TissueBlockLabware tbl = new TissueBlockLabware(); + tbl.setContents(cons); + TissueBlockRequest request = new TissueBlockRequest(); + request.setLabware(List.of(tbl)); + BlockLabwareData ld = new BlockLabwareData(tbl); + ld.getBlocks().forEach(bl -> bl.setSourceLabware(lw1)); + BlockValidatorImp val = makeVal(request); + val.setLwData(List.of(ld)); + val.setProblems(new LinkedHashSet<>()); + + val.loadSourceSamples(); + assertThat(val.getLwData()).containsExactly(ld); + if (ok) { + Zip.of(ld.getBlocks().stream(), Arrays.stream(samples)).forEach((bl, sam) -> assertSame(sam, bl.getSourceSample())); + assertThat(val.getProblems()).isEmpty(); + } else { + ld.getBlocks().forEach(bl -> assertNull(bl.getSourceSample())); + assertThat(val.getProblems()).containsExactlyInAnyOrder("Source sample ID missing from block request.", + "Sample id "+badSampleId+" not present in labware "+lw1.getBarcode()+"."); + } + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testLoadLabwareTypes(boolean ok) { + LabwareType lt1 = EntityFactory.makeLabwareType(1,1,"Alpha"); + LabwareType lt2 = EntityFactory.makeLabwareType(1,1, "Beta"); + List tbls = Stream.of("Alpha", "alpha", "beta", "BETA") + .map(ln -> { + TissueBlockLabware tbl = new TissueBlockLabware(); + tbl.setLabwareType(ln); + return tbl; + }).toList(); + if (!ok) { + tbls.get(1).setLabwareType("Gamma"); + tbls.get(3).setLabwareType(null); + } + List bls = tbls.stream().map(BlockLabwareData::new).toList(); + when(mockLtRepo.findAllByNameIn(any())).thenReturn(List.of(lt1, lt2)); + TissueBlockRequest request = new TissueBlockRequest(tbls); + BlockValidatorImp val = makeVal(request); + val.setProblems(new LinkedHashSet<>()); + val.setLwData(bls); + val.loadLabwareTypes(); + assertThat(val.getLwData()).containsExactlyElementsOf(bls); + assertSame(lt1, bls.get(0).getLwType()); + assertSame(lt2, bls.get(2).getLwType()); + if (ok) { + verify(mockLtRepo).findAllByNameIn(Set.of("ALPHA", "BETA")); + assertSame(lt1, bls.get(1).getLwType()); + assertSame(lt2, bls.get(3).getLwType()); + assertThat(val.getProblems()).isEmpty(); + } else { + verify(mockLtRepo).findAllByNameIn(Set.of("ALPHA", "BETA", "GAMMA")); + assertNull(bls.get(1).getLwType()); + assertNull(bls.get(3).getLwType()); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Missing labware type.", + "Unknown labware types: [\"Gamma\"]"); + } + } + + @Test + void testLoadComments() { + List comments = List.of(new Comment(1, "com1", "cat"), new Comment(2, "com2", "cat")); + List cons = comments.stream().map(comment -> { + TissueBlockContent con = new TissueBlockContent(); + con.setCommentId(comment.getId()); + return con; + }).toList(); + TissueBlockLabware tbl = new TissueBlockLabware(); + tbl.setContents(cons); + BlockLabwareData bld = new BlockLabwareData(tbl); + TissueBlockRequest request = new TissueBlockRequest(List.of(tbl)); + when(mockCommentValidationService.validateCommentIds(any(), any())).thenReturn(comments); + + BlockValidatorImp val = makeVal(request); + val.setLwData(List.of(bld)); + List problems = new ArrayList<>(); + val.setProblems(problems); + val.loadComments(); + ArgumentCaptor> commentIdStreamCaptor = Matchers.streamCaptor(); + verify(mockCommentValidationService).validateCommentIds(same(problems), commentIdStreamCaptor.capture()); + assertThat(commentIdStreamCaptor.getValue()).containsExactly(1,2); + assertThat(problems).isEmpty(); + Zip.of(comments.stream(), bld.getBlocks().stream()) + .forEach((com, bl) -> assertSame(com, bl.getComment())); + } + + private static TissueBlockContent contentForAddresses(Address... addresses) { + TissueBlockContent con = new TissueBlockContent(); + con.setAddresses(Arrays.asList(addresses)); + return con; + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testCheckDestAddresses(boolean ok) { + LabwareType lt1 = EntityFactory.getTubeType(); + LabwareType lt2 = EntityFactory.makeLabwareType(1, 2, "lt2"); + Address A1 = new Address(1, 1), A2 = new Address(1,2); + TissueBlockLabware tbl1 = new TissueBlockLabware(); + TissueBlockLabware tbl2 = new TissueBlockLabware(); + tbl1.setLabwareType(lt1.getName()); + tbl2.setLabwareType(lt2.getName()); + if (ok) { + tbl1.setContents(List.of(contentForAddresses(A1))); + tbl2.setContents(List.of(contentForAddresses(A1, A2))); + } else { + tbl1.setContents(List.of(contentForAddresses(A1), contentForAddresses(A1, A2))); + tbl2.setContents(List.of(contentForAddresses(A1), new TissueBlockContent())); + } + TissueBlockRequest request = new TissueBlockRequest(List.of(tbl1, tbl2)); + + List blds = request.getLabware().stream().map(BlockLabwareData::new).toList(); + blds.get(0).setLwType(lt1); + blds.get(1).setLwType(lt2); + BlockValidatorImp val = makeVal(request); + val.setLwData(blds); + val.setProblems(new LinkedHashSet<>()); + val.checkDestAddresses(); + if (ok) { + assertThat(val.getProblems()).isEmpty(); + } else { + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Slot address A2 not valid in labware type "+lt1.getName()+".", + "Destination slot addresses missing from block request." + ); + } + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testCheckPrebarcodes(boolean ok) { + LabwareType lt1 = EntityFactory.makeLabwareType(1,1, "lt1"); + LabwareType lt2 = EntityFactory.makeLabwareType(1,1, "lt2"); + lt1.setPrebarcoded(false); + lt2.setPrebarcoded(true); + List tbls = IntStream.range(0,5) + .mapToObj(i -> new TissueBlockLabware()) + .toList(); + if (ok) { + IntStream.range(1, tbls.size()).forEach(i -> tbls.get(i).setPreBarcode("preb"+i)); + } else { + tbls.get(0).setPreBarcode("preb1"); + IntStream.range(2, tbls.size()).forEach(i -> tbls.get(i).setPreBarcode("preb"+i)); + tbls.get(2).setPreBarcode("wut!"); + tbls.get(4).setPreBarcode("preb3"); + } + TissueBlockRequest request = new TissueBlockRequest(tbls); + + List blds = request.getLabware().stream().map(BlockLabwareData::new).toList(); + BlockValidatorImp val = makeVal(request); + Zip.enumerate(blds.stream()).forEach((i, bld) -> bld.setLwType(i==0 ? lt1 : lt2)); + val.setLwData(blds); + val.setProblems(new LinkedHashSet<>()); + if (!ok) { + when(mockPrebarcodeValidator.validate(any(), any())).then(invocation -> { + Consumer problemConsumer = invocation.getArgument(1); + String barcode = invocation.getArgument(0); + if (barcode.indexOf('!')>=0) { + problemConsumer.accept("Bad barcode: " + barcode); + return false; + } + return true; + }); + Labware lw1 = EntityFactory.makeEmptyLabware(lt1); + lw1.setBarcode("preb3"); + when(mockLwRepo.findByBarcodeIn(any())).thenReturn(List.of(lw1)); + } + val.checkPrebarcodes(); + if (ok) { + assertThat(val.getProblems()).isEmpty(); + } else { + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "A barcode is not expected for labware type ["+lt1.getName()+"].", + "A barcode is required for labware type ["+lt2.getName()+"].", + "Bad barcode: wut!", + "Barcode already in use: [preb3]", + "Barcode specified multiple times: [PREB3]" + ); + } + verify(mockPrebarcodeValidator, times(4)).validate(any(), any()); + verify(mockLwRepo).findByBarcodeIn(ok ? Set.of("PREB1", "PREB2", "PREB3", "PREB4") + : Set.of("WUT!", "PREB1", "PREB3")); + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testCheckReplicates(boolean ok) { + Donor donor = EntityFactory.getDonor(); + SpatialLocation sl = EntityFactory.getSpatialLocation(); + BioState bs = EntityFactory.getBioState(); + Tissue tis1 = EntityFactory.makeTissue(donor, sl); + tis1.setReplicate("REP"); + Tissue tis2 = EntityFactory.makeTissue(donor, sl); + tis2.setReplicate(null); + Sample sam1 = new Sample(1, null, tis1, bs); + Sample sam2 = new Sample(2, null, tis2, bs); + List bds; + if (ok) { + bds = Zip.of(Stream.of(sam1, sam2), Stream.of("REP", "REP1")) + .map((sam, rep) -> { + BlockData bd = new BlockData(new TissueBlockContent()); + bd.setSourceSample(sam); + bd.getRequestContent().setReplicate(rep); + return bd; + }).toList(); + when(mockReplicateValidator.validate(any(), any())).thenReturn(true); + } else { + bds = Zip.of(Stream.of(sam1, sam2, sam2, sam2, sam2, sam2), + Stream.of("REPX", null, "REP1", "REP1", "REP!", "REP2")) + .map((sam, rep) -> { + BlockData bd = new BlockData(new TissueBlockContent()); + bd.setSourceSample(sam); + bd.getRequestContent().setReplicate(rep); + return bd; + }).toList(); + when(mockReplicateValidator.validate(any(), any())).thenAnswer(invocation -> { + Consumer problemConsumer = invocation.getArgument(1); + String rep = invocation.getArgument(0); + if (rep.indexOf('!') >= 0) { + problemConsumer.accept("Bad replicate: " + rep); + return false; + } + return true; + }); + when(mockTissueRepo.findByDonorIdAndSpatialLocationIdAndReplicate(anyInt(), anyInt(), eqCi("REP2"))) + .thenReturn(List.of(tis2)); + } + BlockValidatorImp val = makeVal(new TissueBlockRequest()); + BlockLabwareData bld = new BlockLabwareData(new TissueBlockLabware()); + bld.setBlocks(bds); + val.setLwData(List.of(bld)); + val.setProblems(new LinkedHashSet<>()); + val.checkReplicates(); + if (ok) { + assertThat(val.getProblems()).isEmpty(); + } else { + String repdesc = String.format("{Donor: %s, Tissue type: %s, Spatial location: %s, Replicate: %%s}", + donor.getDonorName(), tis1.getTissueType().getName(), sl.getCode()); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Bad replicate: REP!", + "Missing replicate for some blocks.", + "Replicate numbers must match the source replicate number where present.", + "Same replicate specified multiple times: ["+String.format(repdesc, "rep1")+"]", + "Replicate already exists in the database: ["+String.format(repdesc, "rep2")+"]" + ); + } + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testCheckDiscardBarcodes(boolean ok) { + List cons = Stream.of("STAN-1", "STAN-2") + .map(bc -> { + TissueBlockContent con = new TissueBlockContent(); + con.setSourceBarcode(bc); + return con; + }).toList(); + TissueBlockLabware tbl = new TissueBlockLabware(); + tbl.setContents(cons); + TissueBlockRequest request = new TissueBlockRequest(List.of(tbl)); + request.setDiscardSourceBarcodes(ok ? List.of("STAN-1") : List.of("STAN-1", "STAN-3", "")); + BlockValidatorImp val = makeVal(request); + val.setProblems(new LinkedHashSet<>()); + val.checkDiscardBarcodes(); + if (ok) { + assertThat(val.getProblems()).isEmpty(); + } else { + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "A null or empty string was supplied as a barcode to discard.", + "The given list of barcodes to discard includes a barcode that is not specified as a source barcode in this request: [\"STAN-3\"]" + ); + } + } + + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testRaiseError(boolean ok) { + BlockValidatorImp val = makeVal(new TissueBlockRequest()); + List problems = ok ? List.of() : List.of("Problem 1", "Problem 2"); + val.setProblems(problems); + if (ok) { + val.raiseError(); + } else { + assertValidationException(val::raiseError, problems); + } + } +} \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/flag/TestFlagLookupService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/flag/TestFlagLookupService.java index b82203b6d..a90c4f101 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/flag/TestFlagLookupService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/flag/TestFlagLookupService.java @@ -234,7 +234,7 @@ void testLabwareFlagPriority_noflags() { SlotSample lwSs = new SlotSample(lw.getFirstSlot(), sample); Ancestry anc = mock(Ancestry.class); when(mockAncestoriser.findAncestry(any())).thenReturn(anc); - Labware otherLw = EntityFactory.makeBlock(sample); + Labware otherLw = EntityFactory.makeTube(sample); Set ancestorSS = Set.of(lwSs, new SlotSample(otherLw.getFirstSlot(), sample)); when(anc.keySet()).thenReturn(ancestorSS); when(mockFlagRepo.findAllByLabwareIdIn(any())).thenReturn(List.of()); @@ -254,7 +254,7 @@ void testLabwareFlagPriority(boolean relevant) { SlotSample lwSs = new SlotSample(lw.getFirstSlot(), sample); Ancestry anc = mock(Ancestry.class); when(mockAncestoriser.findAncestry(any())).thenReturn(anc); - Labware otherLw = EntityFactory.makeBlock(sample); + Labware otherLw = EntityFactory.makeTube(sample); Set ancestorSS = Set.of(lwSs, new SlotSample(otherLw.getFirstSlot(), sample)); when(anc.keySet()).thenReturn(ancestorSS); List flags = List.of( @@ -289,7 +289,7 @@ void testGetLabwareFlagged_multi() { SlotSample lw2Ss = new SlotSample(lw2.getFirstSlot(), sample); Ancestry anc = mock(Ancestry.class); when(mockAncestoriser.findAncestry(any())).thenReturn(anc); - Labware otherLw = EntityFactory.makeBlock(sample); + Labware otherLw = EntityFactory.makeTube(sample); SlotSample blockSs = new SlotSample(otherLw.getFirstSlot(), sample); Set ancSs = Set.of(lw1Ss, lw2Ss, blockSs); when(anc.keySet()).thenReturn(ancSs); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/history/TestHistoryService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/history/TestHistoryService.java index 0dcd3a318..dacb060b4 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/history/TestHistoryService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/history/TestHistoryService.java @@ -1158,7 +1158,7 @@ public void testLoadLabwareFlags() { }) public void testResultDetail(PassFail res, Address address, String expected) { final int slotId = 7; - Slot slot = (address==null ? null : new Slot(slotId, 2, address, null, null, null)); + Slot slot = (address==null ? null : new Slot(slotId, 2, address, null)); Map slotIdMap = (slot==null ? Map.of() : Map.of(slotId, slot)); ResultOp result = new ResultOp(); result.setResult(res); @@ -1211,7 +1211,7 @@ public void testMeasurementDetail(String name, String value, Address address, St Integer slotId = null; if (address != null) { slotId = 70; - Slot slot = new Slot(slotId, 7, address, null, null, null); + Slot slot = new Slot(slotId, 7, address, null); slotIdMap = Map.of(slotId, slot); } Measurement measurement = new Measurement(6, name, value, 4, 5, slotId); @@ -1232,7 +1232,7 @@ public void testDoesCommentApply(OperationComment opCom, int sampleId, int labwa } static Stream doesCommentApplyData() { - Slot slot = new Slot(100, 10, new Address(1,1), null, null, null); + Slot slot = new Slot(100, 10, new Address(1,1), null); Comment com = new Comment(1, "Alabama", "Bananas"); return Stream.of( Arguments.of(new OperationComment(1, com, 1, null, null, null), 1, 10, null, true), @@ -1273,7 +1273,7 @@ static Stream doesMeasurementApplyData() { final int sampleId = 4; final int slotId = 10; final int lwId = 1; - Map slotIdMap = Map.of(slotId, new Slot(slotId, lwId, new Address(1,2), null, null, null)); + Map slotIdMap = Map.of(slotId, new Slot(slotId, lwId, new Address(1,2), null)); return Arrays.stream(new Object[][] { {new Measurement(1, "A", "1", null, 1, null), sampleId, lwId, slotIdMap, true}, {new Measurement(1, "A", "1", sampleId, 1, null), sampleId, lwId, slotIdMap, true}, diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/label/TestLabwareLabelDataService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/label/TestLabwareLabelDataService.java index cbd0459bc..8a25dd6a0 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/label/TestLabwareLabelDataService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/label/TestLabwareLabelDataService.java @@ -112,7 +112,7 @@ public void testLabwareDataPlannedContents() { public void testSlotOrderForLabwareType(String ltName, boolean columnMajor) { LabwareType lt = new LabwareType(null, ltName, 2, 3, null, false); List slots = Address.stream(2, 3) - .map(ad -> new Slot(null, 100, ad, null, null, null)) + .map(ad -> new Slot(null, 100, ad, null)) .sorted(service.slotOrderForLabwareType(lt)) .toList(); if (columnMajor) { diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestAliquotService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestAliquotService.java index 04fac8098..3f1b5ffef 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestAliquotService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestAliquotService.java @@ -271,9 +271,11 @@ static Stream validateRequestArgs() { OperationType analysisOp = EntityFactory.makeOperationType("Floop", null, OperationTypeFlag.ANALYSIS); OperationType sectionOp = EntityFactory.makeOperationType("Section", null, OperationTypeFlag.SOURCE_IS_BLOCK); - Sample sample = new Sample(50, null, EntityFactory.getTissue(), EntityFactory.getBioState()); - Labware block = EntityFactory.makeBlock(sample); - Labware section = EntityFactory.getTube(); + Sample[] samples = EntityFactory.makeSamples(2); + samples[0].setBlockHighestSection(0); + samples[1].setSection("1"); + Labware block = EntityFactory.makeTube(samples[0]); + Labware section = EntityFactory.makeTube(samples[1]); return Arrays.stream(new Object[][]{ {3, aliquotOp, block}, diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java index 2ad00e3c8..9054f2e3d 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java @@ -155,8 +155,8 @@ static Stream validateOpTypeArgs() { OperationType ot3 = EntityFactory.makeOperationType("Blodge", null, OperationTypeFlag.IN_PLACE, OperationTypeFlag.SOURCE_IS_BLOCK); LabwareType lt = EntityFactory.getTubeType(); Sample sam = EntityFactory.getSample(); - Labware block = EntityFactory.makeLabware(lt, sam); - block.getFirstSlot().setBlockSampleId(sam.getId()); + Sample blockSam = Sample.newBlock(sam.getId()+1, sam.getTissue(), sam.getBioState(), 1); + Labware block = EntityFactory.makeLabware(lt, blockSam); Labware tube = EntityFactory.makeLabware(lt, sam); return Arrays.stream(new Object[][] { {ot, tube, null}, @@ -164,9 +164,11 @@ static Stream validateOpTypeArgs() { {"", tube, "No operation type specified."}, {"Bananas", tube, "Unknown operation type: \"Bananas\""}, {ot2, tube, "Operation type Passage cannot be recorded in place."}, + {ot3, block, null}, {ot3, tube, "Operation type Blodge can only be recorded on a block."}, }).map(Arguments::of); } + @Test public void testMakeActions_nobs() { Sample sam1 = EntityFactory.getSample(); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java index 8ac71a782..eb0a52c20 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java @@ -346,9 +346,9 @@ static Stream getOrCreateSampleArguments() { ); BioState rna = new BioState(2, "RNA"); Sample newSection = makeSample(544, 3, tissue1, bio); - Slot sourceSlot = new Slot(null, null, new Address(1,1), List.of(blockSample), null, null); - Slot emptySlot = new Slot(null, null, new Address(1,1), List.of(), null, null); - Slot populousSlot = new Slot(null, null, new Address(1,2), sections, null, null); + Slot sourceSlot = new Slot(null, null, new Address(1,1), List.of(blockSample)); + Slot emptySlot = new Slot(null, null, new Address(1,1), List.of()); + Slot populousSlot = new Slot(null, null, new Address(1,2), sections); return Stream.of( Arguments.of(new PlanAction(null, null, sourceSlot, emptySlot, blockSample, null, null, null), emptySlot, blockSample, false), diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java index df7048172..949bea5eb 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java @@ -23,6 +23,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import static java.util.stream.Collectors.toMap; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -516,11 +517,11 @@ public void testGetPlanActionMap() { .toArray(Sample[]::new); final Address A1 = new Address(1, 1); final Address A2 = new Address(1, 2); - Slot source = new Slot(10, 1, A1, null, null, null); + Slot source = new Slot(10, 1, A1, null); Slot[] slots = { - new Slot(100, 10, A1, Arrays.asList(samples), null, null), - new Slot(101, 10, A2, List.of(samples[0]), null, null), - new Slot(200, 20, A1, Arrays.asList(samples), null, null) + new Slot(100, 10, A1, Arrays.asList(samples)), + new Slot(101, 10, A2, List.of(samples[0])), + new Slot(200, 20, A1, Arrays.asList(samples)) }; PlanAction[] pas = { new PlanAction(51, 1, source, slots[0], samples[0]), @@ -550,11 +551,8 @@ public void testUpdateSourceBlocks() { final Tissue tissue = EntityFactory.getTissue(); for (int i = 0; i < sourceSamples.length; ++i) { final int sampleId = 50 + i; - sourceSamples[i] = new Sample(sampleId, null, tissue, bs); + sourceSamples[i] = Sample.newBlock(sampleId, tissue, bs, 10*i); sourceLabware[i] = EntityFactory.makeLabware(tubeType, sourceSamples[i]); - final Slot slot = sourceLabware[i].getFirstSlot(); - slot.setBlockSampleId(sampleId); - slot.setBlockHighestSection(10*i); } Sample[] sections = new Sample[4]; Labware[] destLabware = new Labware[2]; @@ -585,7 +583,7 @@ public void testUpdateSourceBlocks() { // Just to make sure that if source slots have competing instances, they still get updated correctly: for (int i = 0; i < srcs.length; ++i) { Slot src = srcs[i]; - srcs[i] = new Slot(src.getId(), src.getLabwareId(), src.getAddress(), src.getSamples(), src.getBlockSampleId(), src.getBlockHighestSection()); + srcs[i] = new Slot(src.getId(), src.getLabwareId(), src.getAddress(), src.getSamples()); } List op1Actions = List.of( @@ -594,23 +592,23 @@ public void testUpdateSourceBlocks() { ); ops[1].setActions(op1Actions); - final List updatedSources = new ArrayList<>(2); - when(mockSlotRepo.saveAll(any())).then(invocation -> { - Collection slots = invocation.getArgument(0); - updatedSources.addAll(slots); - - return slots; + final List updatedSamples = new ArrayList<>(2); + when(mockSampleRepo.saveAll(any())).then(invocation -> { + Collection samples = invocation.getArgument(0); + updatedSamples.addAll(samples); + return samples; }); service.updateSourceBlocks(Arrays.asList(ops)); - verify(mockSlotRepo).saveAll(any()); + verify(mockSampleRepo).saveAll(any()); - updatedSources.sort(Comparator.comparing(Slot::getId)); - final List expectedUpdatedSources = List.of( - new Slot(srcs[0].getId(), srcs[0].getLabwareId(), srcs[0].getAddress(), srcs[0].getSamples(), srcs[0].getBlockSampleId(), 21), - new Slot(srcs[1].getId(), srcs[1].getLabwareId(), srcs[1].getAddress(), srcs[1].getSamples(), srcs[1].getBlockSampleId(), 23) + updatedSamples.sort(Comparator.comparing(Sample::getId)); + Map srcSampleHighs = updatedSamples.stream() + .collect(toMap(Sample::getId, Sample::getBlockHighestSection)); + assertEquals( + Map.of(sourceSamples[0].getId(), Integer.valueOf(sections[1].getSection()), sourceSamples[1].getId(), Integer.valueOf(sections[3].getSection())), + srcSampleHighs ); - assertEquals(expectedUpdatedSources, updatedSources); } } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionValidationService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionValidationService.java index 233bae8bb..3900d3b38 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionValidationService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionValidationService.java @@ -411,18 +411,16 @@ public void testValidateSections() { lw1.setBarcode("STAN-001"); Labware lw2 = EntityFactory.makeEmptyLabware(lt); lw2.setBarcode("STAN-002"); - Sample sampleA = EntityFactory.getSample(); + Sample sample0 = EntityFactory.getSample(); + Sample sampleA = Sample.newBlock(sample0.getId()+1, sample0.getTissue(), sample0.getBioState(), 12); Tissue tissueB = EntityFactory.makeTissue(EntityFactory.getDonor(), EntityFactory.getSpatialLocation()); Sample sampleB = new Sample(sampleA.getId()+1, null, tissueB, sampleA.getBioState()); LabwareType tubeType = EntityFactory.getTubeType(); Labware lwA = EntityFactory.makeLabware(tubeType, sampleA); final Slot slotA = lwA.getFirstSlot(); - slotA.setBlockSampleId(sampleA.getId()); - slotA.setBlockHighestSection(12); Labware lwB = EntityFactory.makeLabware(tubeType, sampleB); final Slot slotB = lwB.getFirstSlot(); - slotB.setBlockSampleId(sampleA.getId()); final Address A1 = new Address(1,1); final Address B3 = new Address(2,3); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/plan/TestPlanValidation.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/plan/TestPlanValidation.java index 525051969..1381476d5 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/plan/TestPlanValidation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/plan/TestPlanValidation.java @@ -390,13 +390,11 @@ static Stream sourcesData() { Sample sample = EntityFactory.getSample(); int sectionSampleId = sample.getId(); int blockSampleId = sectionSampleId + 50; - Sample blockSample = new Sample(blockSampleId, null, sample.getTissue(), EntityFactory.getBioState()); + Sample blockSample = Sample.newBlock(blockSampleId, sample.getTissue(), EntityFactory.getBioState(), 0); LabwareType lt = EntityFactory.getTubeType(); Labware block = EntityFactory.makeEmptyLabware(lt); Slot blockSlot = block.getFirstSlot(); blockSlot.getSamples().add(blockSample); - blockSlot.setBlockSampleId(blockSampleId); - blockSlot.setBlockHighestSection(0); Labware nonBlock = EntityFactory.makeEmptyLabware(lt); nonBlock.getFirstSlot().getSamples().add(sample); @@ -534,12 +532,12 @@ static Stream hasDividedLayoutData() { .mapToObj(i -> EntityFactory.makeTissue(EntityFactory.getDonor(), EntityFactory.getSpatialLocation())) .toArray(Tissue[]::new); Sample[] sams = IntStream.range(0,3) - .mapToObj(i -> new Sample(10+i, null, tissues[i], EntityFactory.getBioState())) + .mapToObj(i -> Sample.newBlock(10+i, tissues[i], EntityFactory.getBioState(), 0)) .toArray(Sample[]::new); Labware[] blocks = Arrays.stream(sams) - .map(EntityFactory::makeBlock) + .map(EntityFactory::makeTube) .toArray(Labware[]::new); - Labware someOtherBlock = EntityFactory.makeBlock(sams[0]); + Labware someOtherBlock = EntityFactory.makeTube(sams[0]); UCMap sourceLwMap = UCMap.from(Labware::getBarcode, blocks); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockFieldChecker.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockFieldChecker.java new file mode 100644 index 000000000..5857d6752 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockFieldChecker.java @@ -0,0 +1,102 @@ +package uk.ac.sanger.sccp.stan.service.register; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import uk.ac.sanger.sccp.stan.EntityFactory; +import uk.ac.sanger.sccp.stan.model.Tissue; +import uk.ac.sanger.sccp.stan.request.register.BlockRegisterLabware; +import uk.ac.sanger.sccp.stan.request.register.BlockRegisterSample; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Test {@link BlockFieldChecker} */ +class TestBlockFieldChecker { + + @ParameterizedTest + @CsvSource({ + "false,false", + "false,true", + "true,true", + }) + void testCheck_ok(boolean hasOldDate, boolean hasNewDate) { + BlockFieldChecker checker = new BlockFieldChecker(); + Tissue tissue = EntityFactory.getTissue(); + LocalDate date = LocalDate.of(2020,1,1); + tissue.setCollectionDate(hasOldDate ? date : null); + BlockRegisterLabware brl = new BlockRegisterLabware(); + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setDonorIdentifier(tissue.getDonor().getDonorName()); + brs.setTissueType(tissue.getTissueType().getName()); + brs.setHmdmc(tissue.getHmdmc().getHmdmc()); + brs.setSpatialLocation(tissue.getSpatialLocation().getCode()); + brs.setSpecies(tissue.getDonor().getSpecies().getName()); + brs.setTissueType(tissue.getTissueType().getName()); + brs.setReplicateNumber(tissue.getReplicate()); + brs.setCellClass(tissue.getCellClass().getName()); + brs.setSampleCollectionDate(hasNewDate ? date : null); + + brl.setMedium(tissue.getMedium().getName()); + brl.setFixative(tissue.getFixative().getName()); + + List problems = new ArrayList<>(); + checker.check(problems::add, brl, brs, tissue); + assertThat(problems).isEmpty(); + } + + @Test + void testCheck_bad() { + BlockFieldChecker checker = new BlockFieldChecker(); + Tissue tissue = EntityFactory.getTissue(); + tissue.setCollectionDate(LocalDate.of(2020,1,1)); + BlockRegisterLabware brl = new BlockRegisterLabware(); + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setDonorIdentifier("DONORX"); + brs.setTissueType("TYPEX"); + brs.setHmdmc("HMDMCX"); + brs.setSpatialLocation(4); + brs.setSpecies("SPECIESX"); + brs.setTissueType("TTX"); + brs.setReplicateNumber("RX"); + brs.setCellClass("CCX"); + brs.setSampleCollectionDate(LocalDate.of(2020,1,2)); + + brl.setMedium("MEDIUMX"); + brl.setFixative("FIXX"); + final String fxt = " for existing tissue "+tissue.getExternalName()+"."; + List expectedProblems = List.of( + "Expected donor identifier to be "+tissue.getDonor().getDonorName()+fxt, + "Expected HuMFre number to be "+tissue.getHmdmc().getHmdmc()+fxt, + "Expected tissue type to be "+tissue.getTissueType().getName()+fxt, + "Expected spatial location to be "+tissue.getSpatialLocation().getCode()+fxt, + "Expected replicate number to be "+tissue.getReplicate()+fxt, + "Expected medium to be "+tissue.getMedium().getName()+fxt, + "Expected fixative to be "+tissue.getFixative().getName()+fxt, + "Expected cellular classification to be "+tissue.getCellClass().getName()+fxt, + "Expected sample collection date to be "+tissue.getCollectionDate()+fxt + ); + + List problems = new ArrayList<>(expectedProblems.size()); + checker.check(problems::add, brl, brs, tissue); + assertThat(problems).containsExactlyInAnyOrderElementsOf(expectedProblems); + } + + @ParameterizedTest + @CsvSource({ + "a,A,true", + ",,true", + "a,,false", + ",a,false", + "a,b,false", + "'','',true", + ",'',true", + }) + void testMatch(String oldValue, String newValue, boolean expected) { + assertEquals(expected, BlockFieldChecker.match(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockRegisterService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockRegisterService.java new file mode 100644 index 000000000..6ab32fcb8 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockRegisterService.java @@ -0,0 +1,459 @@ +package uk.ac.sanger.sccp.stan.service.register; + +import org.junit.jupiter.api.*; +import org.mockito.*; +import uk.ac.sanger.sccp.stan.EntityFactory; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.register.*; +import uk.ac.sanger.sccp.stan.service.LabwareService; +import uk.ac.sanger.sccp.stan.service.OperationService; +import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.UCMap; +import uk.ac.sanger.sccp.utils.Zip; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static uk.ac.sanger.sccp.stan.Matchers.*; + +/** Tests {@link BlockRegisterServiceImp} */ +class TestBlockRegisterService { + @Mock + EntityManager mockEntityManager; + @Mock + RegisterValidationFactory mockValidationFactory; + @Mock + DonorRepo mockDonorRepo; + @Mock + TissueRepo mockTissueRepo; + @Mock + SampleRepo mockSampleRepo; + @Mock + SlotRepo mockSlotRepo; + @Mock + BioRiskRepo mockBioRiskRepo; + @Mock + OperationTypeRepo mockOpTypeRepo; + @Mock + LabwareService mockLabwareService; + @Mock + OperationService mockOperationService; + @Mock + WorkService mockWorkService; + @Mock + RegisterClashChecker mockClashChecker; + + @InjectMocks + BlockRegisterServiceImp service; + + private AutoCloseable mocking; + + @BeforeEach + void setup() { + mocking = MockitoAnnotations.openMocks(this); + service = spy(service); + } + + @AfterEach + void cleanup() throws Exception { + mocking.close(); + } + + @Test + void testRegister_none() { + RegisterResult result = service.register(EntityFactory.getUser(), new BlockRegisterRequest()); + verifyNoInteractions(mockClashChecker); + verifyNoInteractions(mockValidationFactory); + verify(service, never()).updateExistingTissues(any(), any()); + verify(service, never()).create(any(), any(), any()); + assertThat(result.getLabware()).isEmpty(); + assertThat(result.getClashes()).isEmpty(); + assertThat(result.getLabwareSolutions()).isEmpty(); + } + + @Test + void testRegister_clash() { + List clashes = List.of(new RegisterClash()); + when(mockClashChecker.findClashes(any(BlockRegisterRequest.class))).thenReturn(clashes); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(new BlockRegisterLabware())); + RegisterResult result = service.register(EntityFactory.getUser(), request); + verify(mockClashChecker).findClashes(request); + verifyNoInteractions(mockValidationFactory); + verify(service, never()).updateExistingTissues(any(), any()); + verify(service, never()).create(any(), any(), any()); + + assertThat(result.getLabware()).isEmpty(); + assertEquals(clashes, result.getClashes()); + assertThat(result.getLabwareSolutions()).isEmpty(); + } + + @Test + void testRegister_problems() { + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(new BlockRegisterLabware())); + when(mockClashChecker.findClashes(any(BlockRegisterRequest.class))).thenReturn(List.of()); + RegisterValidation validation = mock(RegisterValidation.class); + when(mockValidationFactory.createBlockRegisterValidation(any())).thenReturn(validation); + List problems = List.of("Bad thing"); + when(validation.validate()).thenReturn(problems); + assertValidationException(() -> service.register(EntityFactory.getUser(), request), problems); + verify(mockClashChecker).findClashes(request); + verify(mockValidationFactory).createBlockRegisterValidation(request); + verify(validation).validate(); + verify(service, never()).updateExistingTissues(any(), any()); + verify(service, never()).create(any(), any(), any()); + } + + @Test + void testRegister_ok() { + User user = EntityFactory.getUser(); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(new BlockRegisterLabware())); + when(mockClashChecker.findClashes(any(BlockRegisterRequest.class))).thenReturn(List.of()); + RegisterValidation validation = mock(RegisterValidation.class); + when(mockValidationFactory.createBlockRegisterValidation(any())).thenReturn(validation); + when(validation.validate()).thenReturn(List.of()); + doNothing().when(service).updateExistingTissues(any(), any()); + RegisterResult result = new RegisterResult(List.of(EntityFactory.getTube())); + doReturn(result).when(service).create(any(), any(), any()); + assertSame(result, service.register(user, request)); + verify(mockClashChecker).findClashes(request); + verify(mockValidationFactory).createBlockRegisterValidation(request); + verify(validation).validate(); + verify(service).updateExistingTissues(request, validation); + verify(service).create(request, user, validation); + } + + @Test + void testUpdateExistingTissues_none() { + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setSamples(List.of( + brsForExistingTissue("EXT1", LocalDate.of(2024,1,1), false), + brsForExistingTissue("EXT2", null, true) + )); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + RegisterValidation validation = mock(RegisterValidation.class); + + service.updateExistingTissues(request, validation); + verifyNoInteractions(validation); + verifyNoInteractions(mockTissueRepo); + } + + @Test + void testUpdateExistingTissues_some() { + BlockRegisterLabware brl = new BlockRegisterLabware(); + LocalDate newDate = LocalDate.of(2024,1,1); + brl.setSamples(List.of( + brsForExistingTissue("EXT1", LocalDate.of(2024,1,1), true), + brsForExistingTissue("EXT2", LocalDate.of(2023,1,1), false), + brsForExistingTissue("EXT3", null, true) + )); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + RegisterValidation validation = mock(RegisterValidation.class); + Tissue tissue = new Tissue(); + tissue.setExternalName("EXT1"); + when(validation.getTissue("EXT1")).thenReturn(tissue); + + service.updateExistingTissues(request, validation); + verify(mockTissueRepo).saveAll(List.of(tissue)); + verifyNoMoreInteractions(mockTissueRepo); + assertEquals(newDate, tissue.getCollectionDate()); + } + + static BlockRegisterSample brsForExistingTissue(String externalName, LocalDate date, boolean existing) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier(externalName); + brs.setExistingTissue(existing); + brs.setSampleCollectionDate(date); + return brs; + } + + @Test + void testCreateDonors() { + List brss = List.of( + brsForDonor("DONOR1"), + brsForDonor("DONOR2"), + brsForDonor("Donor1"), + brsForDonor("DONORA") + ); + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setSamples(brss); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + + Donor donor1 = makeDonor(null, "DONOR1"); + Donor donor2 = makeDonor(null, "DONOR2"); + Donor donorA = makeDonor(1, "DONORA"); + List donors = List.of(donor1, donor2, donorA); + RegisterValidation val = mock(RegisterValidation.class); + donors.forEach(d -> when(val.getDonor(eqCi(d.getDonorName()))).thenReturn(d)); + + final int[] idCounter = {5}; + when(mockDonorRepo.save(any())).then(invocation -> { + Donor d = invocation.getArgument(0); + assertNull(d.getId()); + d.setId(++idCounter[0]); + return d; + }); + + UCMap donorMap = service.createDonors(request, val); + assertThat(donorMap).hasSize(donors.size()); + donors.forEach(d -> assertSame(d, donorMap.get(d.getDonorName()))); + verify(mockDonorRepo).save(donor1); + verify(mockDonorRepo).save(donor2); + verifyNoMoreInteractions(mockDonorRepo); + assertEquals(1, donorA.getId()); + assertNotNull(donor1.getId()); + assertNotNull(donor2.getId()); + assertNotEquals(donor1.getId(), donor2.getId()); + } + + static BlockRegisterSample brsForDonor(String donorName) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setDonorIdentifier(donorName); + return brs; + } + + @Test + void testCreateTissues() { + List donors = IntStream.of(1,2).mapToObj(i -> makeDonor(i, "DONOR"+i)).toList(); + UCMap donorMap = UCMap.from(donors, Donor::getDonorName); + doReturn(donorMap).when(service).createDonors(any(), any()); + List hmdmcs = List.of(new Hmdmc(1, "2000/1"), new Hmdmc(2, "2000/2")); + RegisterValidation val = mock(RegisterValidation.class); + hmdmcs.forEach(h -> when(val.getHmdmc(h.getHmdmc())).thenReturn(h)); + List cellClasses = List.of(new CellClass(1, "CC1", false, true), new CellClass(2, "CC2", false, true)); + cellClasses.forEach(cc -> when(val.getCellClass(eqCi(cc.getName()))).thenReturn(cc)); + TissueType tt = new TissueType(1, "TT1", "TT1"); + tt.setSpatialLocations(List.of(new SpatialLocation(10, "SL0", 0, tt), + new SpatialLocation(11, "SL1", 1, tt))); + tt.getSpatialLocations().forEach(sl -> when(val.getSpatialLocation("TT1", sl.getCode())).thenReturn(sl)); + List mediums = List.of(new Medium(1, "med1"), new Medium(2, "med2")); + mediums.forEach(m -> when(val.getMedium(eqCi(m.getName()))).thenReturn(m)); + List fixs = List.of(new Fixative(1, "fix1"), new Fixative(2, "fix2")); + fixs.forEach(f -> when(val.getFixative(eqCi(f.getName()))).thenReturn(f)); + + BlockRegisterSample brs1 = new BlockRegisterSample(); + brs1.setExternalIdentifier("EXT1"); + brs1.setHmdmc("2000/1"); + brs1.setCellClass("CC1"); + brs1.setReplicateNumber("R1"); + brs1.setSampleCollectionDate(LocalDate.of(2026,1,1)); + brs1.setTissueType("TT1"); + brs1.setSpatialLocation(0); + brs1.setDonorIdentifier("DONOR1"); + + BlockRegisterSample brs2 = new BlockRegisterSample(); + brs2.setExternalIdentifier("EXT2"); + brs2.setHmdmc("2000/2"); + brs2.setCellClass("CC2"); + brs2.setReplicateNumber("R2"); + brs2.setSampleCollectionDate(LocalDate.of(2026,1,2)); + brs2.setTissueType("TT1"); + brs2.setSpatialLocation(1); + brs2.setDonorIdentifier("DONOR2"); + + BlockRegisterSample brs3 = new BlockRegisterSample(); + brs3.setExternalIdentifier("EXT3"); + brs3.setHmdmc("2000/1"); + brs3.setCellClass("CC1"); + brs3.setReplicateNumber("R1"); + brs3.setSampleCollectionDate(LocalDate.of(2026,1,3)); + brs3.setTissueType("TT1"); + brs3.setSpatialLocation(0); + brs3.setDonorIdentifier("DONOR1"); + + Tissue existingTissue = new Tissue(); + existingTissue.setId(1); + existingTissue.setExternalName("EXT3"); + when(val.getTissue(eqCi("EXT3"))).thenReturn(existingTissue); + + final int[] idCounter = {5}; + when(mockTissueRepo.save(any())).then(invocation -> { + Tissue t = invocation.getArgument(0); + assertNull(t.getId()); + t.setId(++idCounter[0]); + return t; + }); + + BlockRegisterLabware brl1 = new BlockRegisterLabware(); + brl1.setSamples(List.of(brs1)); + brl1.setMedium("med1"); + brl1.setFixative("fix1"); + BlockRegisterLabware brl2 = new BlockRegisterLabware(); + brl2.setSamples(List.of(brs2, brs3)); + brl2.setMedium("med2"); + brl2.setFixative("fix2"); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl1, brl2)); + + UCMap tissueMap = service.createTissues(request, val); + + assertThat(tissueMap).hasSize(3); + assertSame(existingTissue, tissueMap.get("EXT3")); + assertEquals(1, existingTissue.getId()); + + List newTissues = Stream.of("EXT1", "EXT2").map(tissueMap::get).toList(); + for (int i = 0; i < newTissues.size(); i++) { + Tissue tis = newTissues.get(i); + assertNotNull(tis); + assertNotNull(tis.getId()); + assertSame(mediums.get(i), tis.getMedium()); + assertSame(fixs.get(i), tis.getFixative()); + assertSame(donors.get(i), tis.getDonor()); + assertSame(hmdmcs.get(i), tis.getHmdmc()); + assertSame(cellClasses.get(i), tis.getCellClass()); + assertSame(tt, tis.getTissueType()); + assertSame(tt.getSpatialLocations().get(i), tis.getSpatialLocation()); + assertEquals(request.getLabware().get(i).getSamples().getFirst().getSampleCollectionDate(), tis.getCollectionDate()); + assertEquals("EXT"+(i+1), tis.getExternalName()); + assertEquals("r"+(i+1), tis.getReplicate()); + } + assertNotEquals(newTissues.get(0).getId(), newTissues.get(1).getId()); + verify(mockTissueRepo, times(2)).save(any()); + } + + @Test + void testCreate() { + User user = EntityFactory.getUser(); + List works = List.of(EntityFactory.makeWork("SGP1")); + RegisterValidation val = mock(RegisterValidation.class); + when(val.getWorks()).thenReturn(works); + Tissue[] tissues = IntStream.of(1,2,3).mapToObj(i -> makeTissue(i, "EXT"+i)).toArray(Tissue[]::new); + doReturn(UCMap.from(Tissue::getExternalName, tissues)).when(service).createTissues(any(), any()); + OperationType opType = EntityFactory.makeOperationType("Register", EntityFactory.getBioState()); + when(mockOpTypeRepo.getByName(eqCi(opType.getName()))).thenReturn(opType); + LabwareType lt = new LabwareType(1, "lt", 1, 2, null, false); + when(val.getLabwareType(eqCi(lt.getName()))).thenReturn(lt); + BioRisk[] bioRisks = IntStream.of(1,2,3).mapToObj(i -> new BioRisk(i, "BR"+i)).toArray(BioRisk[]::new); + Arrays.stream(bioRisks).forEach(br -> when(val.getBioRisk(eqCi(br.getCode()))).thenReturn(br)); + Labware[] lws = Stream.of("XB1", "XB2").map(xb -> { + Labware lw = EntityFactory.makeEmptyLabware(lt); + lw.setExternalBarcode(xb); + lw.setBarcode(xb); + when(mockLabwareService.create(any(), eqCi(xb), eqCi(xb))).thenReturn(lw); + return lw; + }).toArray(Labware[]::new); + + final int[] idCounter = {20}; + when(mockSampleRepo.save(any())).then(invocation -> { + Sample sam = invocation.getArgument(0); + assertNull(sam.getId()); + sam.setId(++idCounter[0]); + return sam; + }); + + Operation[] ops = IntStream.of(100,101).mapToObj(i -> { + Operation op = new Operation(); + op.setOperationType(opType); + op.setId(i); + return op; + }).toArray(Operation[]::new); + when(mockOperationService.createOperation(any(), any(), any(), isNull())).thenReturn(ops[0], ops[1]); + + final Address A1 = new Address(1,1), A2 = new Address(1,2); + + List brs1 = List.of( + brsForCreate("EXT1", 31, "BR1", List.of(A1, A2)) + ); + List brs2 = List.of( + brsForCreate("EXT2", 32, "BR2", List.of(A1)), + brsForCreate("EXT3", 33, "BR3", List.of(A2)) + ); + BlockRegisterLabware brl1 = brlForCreate(lt.getName(), lws[0].getExternalBarcode(), brs1); + BlockRegisterLabware brl2 = brlForCreate(lt.getName(), lws[1].getExternalBarcode(), brs2); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl1, brl2)); + + RegisterResult result = service.create(request, user, val); + + verify(service).createTissues(request, val); + verify(mockOpTypeRepo).getByName(eqCi(opType.getName())); + for (Labware lw : lws) { + verify(mockLabwareService).create(lt, lw.getExternalBarcode(), lw.getBarcode()); + } + verifyNoMoreInteractions(mockLabwareService); + verify(mockSampleRepo, times(3)).save(any()); + Arrays.stream(lws).forEach(lw -> verify(mockEntityManager).refresh(lw)); + verify(mockSlotRepo).saveAll(Set.of(lws[0].getSlot(A1), lws[0].getSlot(A2))); + verify(mockSlotRepo).saveAll(Set.of(lws[1].getSlot(A1), lws[1].getSlot(A2))); + verifyNoMoreInteractions(mockSlotRepo); + ArgumentCaptor> actionsCaptor = genericCaptor(List.class); + verify(mockOperationService, times(2)).createOperation(same(opType), same(user), + actionsCaptor.capture(), isNull()); + verifyNoMoreInteractions(mockOperationService); + verify(mockWorkService).link(same(works), eq(Arrays.asList(ops))); + verifyNoMoreInteractions(mockWorkService); + List> actionses = actionsCaptor.getAllValues(); + assertThat(actionses).hasSize(2); + List actions = actionses.getFirst(); + assertThat(actions).hasSize(2); + List samples = new ArrayList<>(3); + Sample sam1 = actions.getFirst().getSample(); + samples.add(sam1); + Zip.of(actions.stream(), lws[0].getSlots().stream()).forEach((a, slot) -> { + assertSame(slot, a.getSource()); + assertSame(slot, a.getDestination()); + assertSame(sam1, a.getSourceSample()); + assertSame(sam1, a.getSample()); + }); + actions = actionses.get(1); + assertThat(actions).hasSize(2); + Zip.of(actions.stream(), lws[1].getSlots().stream()).forEach((a, slot) -> { + assertSame(slot, a.getSource()); + assertSame(slot, a.getDestination()); + assertSame(a.getSample(), a.getSourceSample()); + samples.add(a.getSample()); + }); + + int[] opIds = {100, 101, 101}; + Zip.enumerate(samples.stream()).forEach((i, sam) -> { + assertNotNull(sam.getId()); + assertEquals(31+i, sam.getBlockHighestSection()); + assertSame(tissues[i], sam.getTissue()); + assertSame(opType.getNewBioState(), sam.getBioState()); + verify(mockBioRiskRepo).recordBioRisk(sam, bioRisks[i], opIds[i]); + }); + verifyNoMoreInteractions(mockBioRiskRepo); + assertThat(result.getLabware()).containsExactly(lws); + } + + static BlockRegisterLabware brlForCreate(String lt, String xb, List samples) { + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setLabwareType(lt); + brl.setExternalBarcode(xb); + brl.setSamples(samples); + return brl; + } + + static BlockRegisterSample brsForCreate(String extName, Integer highestSection, String bioRiskCode, + List
addresses) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier(extName); + brs.setHighestSection(highestSection); + brs.setBioRiskCode(bioRiskCode); + brs.setAddresses(addresses); + return brs; + } + + static Tissue makeTissue(Integer id, String extName) { + Tissue tis = new Tissue(); + tis.setId(id); + tis.setExternalName(extName); + return tis; + } + + static Donor makeDonor(Integer id, String name) { + return new Donor(id, name, LifeStage.adult, EntityFactory.getHuman()); + } +} diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockRegisterValidation.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockRegisterValidation.java new file mode 100644 index 000000000..8a10daf73 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestBlockRegisterValidation.java @@ -0,0 +1,730 @@ +package uk.ac.sanger.sccp.stan.service.register; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.*; +import uk.ac.sanger.sccp.stan.EntityFactory; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.*; +import uk.ac.sanger.sccp.stan.request.register.*; +import uk.ac.sanger.sccp.stan.service.BioRiskService; +import uk.ac.sanger.sccp.stan.service.Validator; +import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.UCMap; +import uk.ac.sanger.sccp.utils.Zip; + +import java.time.LocalDate; +import java.util.*; +import java.util.function.*; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static uk.ac.sanger.sccp.stan.Matchers.*; +import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty; + +class TestBlockRegisterValidation { + @Mock + DonorRepo mockDonorRepo; + @Mock + HmdmcRepo mockHmdmcRepo; + @Mock + TissueTypeRepo mockTtRepo; + @Mock + LabwareTypeRepo mockLtRepo; + @Mock + MediumRepo mockMediumRepo; + @Mock + FixativeRepo mockFixativeRepo; + @Mock + TissueRepo mockTissueRepo; + @Mock + SpeciesRepo mockSpeciesRepo; + @Mock + CellClassRepo mockCellClassRepo; + @Mock + LabwareRepo mockLwRepo; + @Mock + Validator mockDonorNameValidator; + @Mock + Validator mockExternalNameValidator; + @Mock + Validator mockReplicateValidator; + @Mock + Validator mockExternalBarcodeValidator; + @Mock + BlockFieldChecker mockBlockFieldChecker; + @Mock + BioRiskService mockBioRiskService; + @Mock + WorkService mockWorkService; + + private AutoCloseable mocking; + + @BeforeEach + void setup() { + mocking = MockitoAnnotations.openMocks(this); + } + + @AfterEach + void cleanup() throws Exception { + mocking.close(); + } + + static BlockRegisterRequest toRequest(List brss) { + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setSamples(brss); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + return request; + } + + BlockRegisterValidationImp makeVal(BlockRegisterRequest request) { + return spy(new BlockRegisterValidationImp(request, mockDonorRepo, mockHmdmcRepo, mockTtRepo, mockLtRepo, + mockMediumRepo, mockFixativeRepo, mockTissueRepo, mockSpeciesRepo, mockCellClassRepo, mockLwRepo, + mockDonorNameValidator, mockExternalNameValidator, mockReplicateValidator, mockExternalBarcodeValidator, + mockBlockFieldChecker, mockBioRiskService, mockWorkService)); + } + + private static List> valMethods() { + return List.of(BlockRegisterValidationImp::validateDonors, + BlockRegisterValidationImp::validateHmdmcs, + BlockRegisterValidationImp::validateSpatialLocations, + BlockRegisterValidationImp::validateLabwareTypes, + BlockRegisterValidationImp::validateExternalBarcodes, + BlockRegisterValidationImp::validateAddresses, + BlockRegisterValidationImp::validateMediums, + BlockRegisterValidationImp::validateFixatives, + BlockRegisterValidationImp::validateCollectionDates, + BlockRegisterValidationImp::validateExistingTissues, + BlockRegisterValidationImp::validateNewTissues, + BlockRegisterValidationImp::validateBioRisks, + BlockRegisterValidationImp::validateWorks, + BlockRegisterValidationImp::validateCellClasses + ); + } + + @Test + void testValidate_empty() { + BlockRegisterRequest request = new BlockRegisterRequest(); + BlockRegisterValidationImp val = makeVal(request); + var problems = val.validate(); + for (var method : valMethods()) { + method.accept(verify(val, never())); + } + assertThat(problems).containsExactly("No labware specified in request."); + } + + @Test + void testValidate() { + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(new BlockRegisterLabware())); + request.getLabware().getFirst().setSamples(List.of(new BlockRegisterSample())); + BlockRegisterValidationImp val = makeVal(request); + for (var method : valMethods()) { + method.accept(doNothing().when(val)); + } + assertThat(val.validate()).isEmpty(); + InOrder inOrder = inOrder(val); + for (var method : valMethods()) { + method.accept(inOrder.verify(val)); + } + } + + @Test + void testValidateDonors_problems() { + List brss = List.of( + brsForDonor(null, "Human", LifeStage.adult), + brsForDonor("Donor!", "Human", LifeStage.adult), + brsForDonor("DONOR1", null, LifeStage.adult), + brsForDonor("DONOR2", "Humming", LifeStage.adult), + brsForDonor("DONOR3", "Mutant", LifeStage.adult), + brsForDonor("DONOR4", "Human", LifeStage.adult), + brsForDonor("Donor4", "Hamster", LifeStage.adult), + brsForDonor("DONOR5", "Human", LifeStage.adult), + brsForDonor("DONOR5", "Human", LifeStage.fetal), + brsForDonor("DONORA", "Hamster", LifeStage.adult), + brsForDonor("DONORB", "Human", LifeStage.fetal) + ); + BlockRegisterRequest request = toRequest(brss); + + Species human = new Species(1, "Human"); + Species hamster = new Species(2, "Hamster"); + Species mutant = new Species(3, "Mutant"); + mutant.setEnabled(false); + when(mockSpeciesRepo.findByName(anyString())).thenReturn(Optional.empty()); + Stream.of(human, hamster, mutant).forEach(s -> doReturn(Optional.of(s)).when(mockSpeciesRepo).findByName(eqCi(s.getName()))); + Donor donora = new Donor(10, "DONORA", LifeStage.adult, human); + Donor donorb = new Donor(11, "DONORB", LifeStage.adult, human); + when(mockDonorRepo.findByDonorName(anyString())).thenReturn(Optional.empty()); + Stream.of(donora, donorb).forEach(d -> doReturn(Optional.of(d)).when(mockDonorRepo).findByDonorName(eqCi(d.getDonorName()))); + mockStringValidator(mockDonorNameValidator, "donor name"); + BlockRegisterValidationImp val = makeVal(request); + val.validateDonors(); + var problems = val.getProblems(); + assertThat(problems).containsExactlyInAnyOrder( + "Missing donor identifier.", + "Bad donor name: Donor!", + "Missing species.", + "Unknown species: \"Humming\"", + "Species is not enabled: Mutant", + "Multiple different species specified for donor DONOR4", + "Multiple different life stages specified for donor DONOR5", + "Wrong species given for existing donor DONORA", + "Wrong life stage given for existing donor DONORB" + ); + assertSame(donora, val.getDonor("donorA")); + assertSame(donorb, val.getDonor("donorb")); + assertNotNull(val.getDonor("donor4")); + } + + @Test + void testValidateDonors_ok() { + List brss = List.of( + brsForDonor("donor1", "Human", LifeStage.adult), + brsForDonor("DONOR1", "Human", LifeStage.adult), + brsForDonor("donora", "Human", LifeStage.adult) + ); + Species human = new Species(1, "Human"); + when(mockSpeciesRepo.findByName(eqCi(human.getName()))).thenReturn(Optional.of(human)); + Donor donora = new Donor(1, "DONORA", LifeStage.adult, human); + when(mockDonorRepo.findByDonorName(anyString())).thenReturn(Optional.empty()); + doReturn(Optional.of(donora)).when(mockDonorRepo).findByDonorName(eqCi(donora.getDonorName())); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateDonors(); + assertSame(donora, val.getDonor("DonorA")); + assertNotNull(val.getDonor("Donor1")); + assertThat(val.getProblems()).isEmpty(); + } + + private static BlockRegisterSample brsForDonor(String donorName, String species, LifeStage lifeStage) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setDonorIdentifier(donorName); + brs.setSpecies(species); + brs.setLifeStage(lifeStage); + return brs; + } + + @Test + void testValidateHmdmcs_problems() { + final String HUMAN_NAME = Species.HUMAN_NAME; + Hmdmc hmdmc0 = new Hmdmc(10, "20/000"); + Hmdmc hmdmcX = new Hmdmc(11, "20/001"); + hmdmcX.setEnabled(false); + when(mockHmdmcRepo.findByHmdmc(anyString())).thenReturn(Optional.empty()); + Stream.of(hmdmc0, hmdmcX).forEach(h -> doReturn(Optional.of(h)).when(mockHmdmcRepo).findByHmdmc(h.getHmdmc())); + + List brss = List.of( + brsForHmdmc(null, HUMAN_NAME), + brsForHmdmc("20/000", HUMAN_NAME), + brsForHmdmc("20/000", "Hamster"), + brsForHmdmc("20/001", HUMAN_NAME), + brsForHmdmc("20/002", HUMAN_NAME) + ); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateHmdmcs(); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Missing HuMFre number.", "Non-human tissue should not have a HuMFre number.", + "Unknown HuMFre number: [20/002]", "HuMFre number not enabled: [20/001]" + ); + assertSame(hmdmc0, val.getHmdmc("20/000")); + assertSame(hmdmcX, val.getHmdmc("20/001")); + } + + @Test + void testValidateHmdmcs_ok() { + final String HUMAN_NAME = Species.HUMAN_NAME; + Hmdmc hmdmc0 = new Hmdmc(10, "20/000"); + when(mockHmdmcRepo.findByHmdmc(hmdmc0.getHmdmc())).thenReturn(Optional.of(hmdmc0)); + + List brss = List.of( + brsForHmdmc("20/000", HUMAN_NAME), + brsForHmdmc(null, "Hamster") + ); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateHmdmcs(); + assertThat(val.getProblems()).isEmpty(); + assertSame(hmdmc0, val.getHmdmc("20/000")); + } + + private static BlockRegisterSample brsForHmdmc(String hmdmc, String speciesName) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setHmdmc(hmdmc); + brs.setSpecies(speciesName); + return brs; + } + + @Test + void testValidateSpatialLocations_problems() { + List brss = List.of( + brsForSL(null, 0), // missing tissue type + brsForSL("Legg", 0), // unknown tissue type + brsForSL("Tail", 0), // disabled tissue type + brsForSL("Leg", 13), // unknown spatial location + brsForSL("Leg", 1), // disabled spatial location + brsForSL("LEG", 0) // ok + ); + TissueType tail = new TissueType(1, "Tail", "Tail"); + tail.setSpatialLocations(List.of(new SpatialLocation(0, "Alpha", 0, tail))); + tail.setEnabled(false); + TissueType leg = new TissueType(2, "Leg", "Leg"); + leg.setSpatialLocations(List.of(new SpatialLocation(20, "Alpha", 0, leg), + new SpatialLocation(21, "Beta", 1, leg))); + leg.getSpatialLocations().getLast().setEnabled(false); + when(mockTtRepo.findByName(anyString())).thenReturn(Optional.empty()); + Stream.of(tail, leg).forEach(tt -> doReturn(Optional.of(tt)).when(mockTtRepo).findByName(eqCi(tt.getName()))); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateSpatialLocations(); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Missing tissue type.", + "Unknown tissue type: [Legg]", + "Tissue type \"Tail\" is disabled.", + "Unknown spatial location 13 for tissue type Leg.", + "Spatial location is disabled: 1 for tissue type Leg." + ); + assertSame(leg.getSpatialLocations().getFirst(), val.getSpatialLocation("LEG", 0)); + } + + @Test + void testValidateSpatialLocations_ok() { + List brss = List.of(brsForSL("Leg", 0)); + TissueType leg = new TissueType(1, "Leg", "Leg"); + leg.setSpatialLocations(List.of(new SpatialLocation(10, "Alpha", 0, leg))); + when(mockTtRepo.findByName(eqCi(leg.getName()))).thenReturn(Optional.of(leg)); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateSpatialLocations(); + assertThat(val.getProblems()).isEmpty(); + assertSame(leg.getSpatialLocations().getFirst(), val.getSpatialLocation("LEG", 0)); + } + + private static BlockRegisterSample brsForSL(String ttName, int slCode) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setTissueType(ttName); + brs.setSpatialLocation(slCode); + return brs; + } + + @Test + void testValidateLabwareTypes() { + testGeneric(EntityFactory.getTubeType(), LabwareType::getName, "labware type", + mockLtRepo, BlockRegisterLabware::setLabwareType, LabwareTypeRepo::findByName, + BlockRegisterValidationImp::validateLabwareTypes, + BlockRegisterValidationImp::getLabwareType); + } + + @Test + void testValidateMediums() { + testGeneric(EntityFactory.getMedium(), Medium::getName, "medium", + mockMediumRepo, BlockRegisterLabware::setMedium, MediumRepo::findByName, + BlockRegisterValidationImp::validateMediums, + BlockRegisterValidationImp::getMedium); + } + + @Test + void testValidateFixatives() { + testGeneric(EntityFactory.getFixative(), Fixative::getName, "fixative", + mockFixativeRepo, BlockRegisterLabware::setFixative, FixativeRepo::findByName, + BlockRegisterValidationImp::validateFixatives, + BlockRegisterValidationImp::getFixative); + } + + void testGeneric(E validEntity, Function entityNameGetter, String entityTypeName, + R repo, + BiConsumer nameSetter, + BiFunction> lkp, + Consumer valMethod, + BiFunction retriever) { + String entityName = entityNameGetter.apply(validEntity); + lkp.apply(doReturn(Optional.empty()).when(repo), any()); + lkp.apply(doReturn(Optional.of(validEntity)).when(repo), eqCi(entityName)); + + BlockRegisterLabware brl = new BlockRegisterLabware(); + nameSetter.accept(brl, entityName); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + BlockRegisterValidationImp val = makeVal(request); + valMethod.accept(val); + assertThat(val.getProblems()).isEmpty(); + assertSame(validEntity, retriever.apply(val, entityName)); + + brl = new BlockRegisterLabware(); + nameSetter.accept(brl, "invalid"); + request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + val = makeVal(request); + valMethod.accept(val); + assertThat(val.getProblems()).containsExactly("Unknown " + entityTypeName + ": [\"invalid\"]"); + + brl = new BlockRegisterLabware(); + nameSetter.accept(brl, null); + request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + val = makeVal(request); + valMethod.accept(val); + assertThat(val.getProblems()).containsExactly("Missing " + entityTypeName + "."); + } + + + @Test + void testValidateCollectionDates_problems() { + String HUMAN_NAME = Species.HUMAN_NAME; + LocalDate today = LocalDate.now(); + LocalDate invalidDate = today.plusDays(2L); + List brss = List.of( + brsForDate("EXT1", HUMAN_NAME, LifeStage.fetal, null), // date missing + brsForDate("ext2", HUMAN_NAME, LifeStage.fetal, invalidDate), // bad date + brsForDate("ext3", HUMAN_NAME, LifeStage.fetal, today.minusDays(2L)), + brsForDate("Ext3", HUMAN_NAME, LifeStage.fetal, today.minusDays(3L)) + ); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateCollectionDates(); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Human fetal samples must have a collection date.", + "Invalid sample collection date: ["+invalidDate+"]", + "Inconsistent collection dates specified for tissue EXT3." + ); + } + + @Test + void testValidateCollectionDates_ok() { + String HUMAN_NAME = Species.HUMAN_NAME; + LocalDate validDate = LocalDate.now().minusDays(2L); + List brss = List.of( + brsForDate("EXT1", HUMAN_NAME, LifeStage.fetal, validDate), + brsForDate("Ext1", HUMAN_NAME, LifeStage.fetal, validDate), // matching date + brsForDate("ext2", HUMAN_NAME, LifeStage.adult, null), + brsForDate("Ext3", "Hamster", LifeStage.fetal, null) + ); + BlockRegisterRequest request = toRequest(brss); + BlockRegisterValidationImp val = makeVal(request); + val.validateCollectionDates(); + assertThat(val.getProblems()).isEmpty(); + } + + static BlockRegisterSample brsForDate(String externalName, String species, LifeStage ls, LocalDate date) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier(externalName); + brs.setSpecies(species); + brs.setLifeStage(ls); + brs.setSampleCollectionDate(date); + return brs; + } + + @Test + void testValidateExistingTissues_problems() { + BlockRegisterLabware brl1 = new BlockRegisterLabware(); + brl1.setSamples(List.of( + brsForExistingTissue("Ext1", true), + brsForExistingTissue("EXT404", true) + )); + BlockRegisterLabware brl2 = new BlockRegisterLabware(); + brl2.setSamples(List.of(brsForExistingTissue("ext2", true), + brsForExistingTissue(null, true), + brsForExistingTissue("Exta", false) + )); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl1, brl2)); + + Tissue ext1 = tissueWithExtName("EXT1"); + Tissue ext2 = tissueWithExtName("EXT2"); + when(mockTissueRepo.findAllByExternalNameIn(any())).thenReturn(List.of(ext1, ext2)); + + doAnswer(invocation -> { + Consumer problemConsumer = invocation.getArgument(0); + problemConsumer.accept("Check failed for "+ext2.getExternalName()); + return null; + }).when(mockBlockFieldChecker).check(any(), any(), any(), same(ext2)); + + var val = makeVal(request); + val.validateExistingTissues(); + + verify(mockTissueRepo).findAllByExternalNameIn(Set.of("Ext1", "EXT404", "ext2")); + + verify(mockBlockFieldChecker).check(any(), same(brl1), same(brl1.getSamples().getFirst()), same(ext1)); + verify(mockBlockFieldChecker).check(any(), same(brl2), same(brl2.getSamples().getFirst()), same(ext2)); + verifyNoMoreInteractions(mockBlockFieldChecker); + + assertSame(ext1, val.getTissue("Ext1")); + assertSame(ext2, val.getTissue("ext2")); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Missing external identifier.", + "Existing external identifier not recognised: [\"EXT404\"]", + "Check failed for EXT2" + ); + } + + @Test + void testValidateExistingTissues_ok() { + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setSamples(List.of(brsForExistingTissue("Ext1", true), brsForExistingTissue("ExtA", false))); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + Tissue ext1 = tissueWithExtName("EXT1"); + when(mockTissueRepo.findAllByExternalNameIn(any())).thenReturn(List.of(ext1)); + var val = makeVal(request); + val.validateExistingTissues(); + assertThat(val.getProblems()).isEmpty(); + assertSame(ext1, val.getTissue("Ext1")); + verify(mockTissueRepo).findAllByExternalNameIn(Set.of("Ext1")); + verify(mockBlockFieldChecker).check(any(), same(brl), same(brl.getSamples().getFirst()), same(ext1)); + verifyNoMoreInteractions(mockBlockFieldChecker); + } + + static BlockRegisterSample brsForExistingTissue(String externalName, boolean existing) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier(externalName); + brs.setExistingTissue(existing); + return brs; + } + + static Tissue tissueWithExtName(String externalName) { + Tissue t = EntityFactory.makeTissue(EntityFactory.getDonor(), EntityFactory.getSpatialLocation()); + t.setExternalName(externalName); + return t; + } + + @Test + void testValidateNewTissues() { + List brss = List.of( + brsForNewTissue("Ext1", "R1", -1), // negative highest section + brsForNewTissue("Ext2", "", 1), // missing repl + brsForNewTissue(null, "R1", 1), // missing external name + brsForExistingTissue("EXTA", true), // Existing--skip + brsForNewTissue("EXTB", "R1", 1), // exists unexpectedly + brsForNewTissue("EXT3", "R1", 1), + brsForNewTissue("Ext3", "R1", 1), // repeated + brsForNewTissue("EXT!", "R1", 1), // bad ext name + brsForNewTissue("EXT4", "R!", 1) // bad repl + ); + BlockRegisterRequest request = toRequest(brss); + mockStringValidator(mockExternalNameValidator, "external name"); + mockStringValidator(mockReplicateValidator, "replicate"); + Tissue tis = tissueWithExtName("EXTB"); + when(mockTissueRepo.findAllByExternalName(eqCi("EXTB"))).thenReturn(List.of(tis)); + + var val = makeVal(request); + val.validateNewTissues(); + verify(mockReplicateValidator, times(6)).validate(eq("R1"), any()); + verify(mockReplicateValidator, times(1)).validate(eq("R!"), any()); + verifyNoMoreInteractions(mockReplicateValidator); + Stream.of("Ext1", "Ext2", "EXTB", "EXT3", "Ext3", "EXT!", "EXT4") + .forEach(xn -> verify(mockExternalNameValidator).validate(eq(xn), any())); + verifyNoMoreInteractions(mockExternalNameValidator); + + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Highest section number cannot be negative.", + "Missing replicate number.", + "Missing external identifier.", + "Repeated external identifier: Ext3", + "Bad external name: EXT!", + "Bad replicate: R!", + "There is already tissue in the database with external identifier EXTB." + ); + } + + @Test + void testValidateNewTissues_ok() { + List brss = List.of( + brsForNewTissue("Ext1", "R1", 1), + brsForExistingTissue("EXTA", true) + ); + BlockRegisterRequest request = toRequest(brss); + var val = makeVal(request); + val.validateNewTissues(); + verify(mockReplicateValidator).validate(eq("R1"), any()); + verifyNoMoreInteractions(mockReplicateValidator); + verify(mockExternalNameValidator).validate(eq("Ext1"), any()); + verifyNoMoreInteractions(mockExternalNameValidator); + verify(mockTissueRepo).findAllByExternalName(eqCi("Ext1")); + verifyNoMoreInteractions(mockTissueRepo); + assertThat(val.getProblems()).isEmpty(); + } + + static BlockRegisterSample brsForNewTissue(String extName, String repl, Integer highestSec) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier(extName); + brs.setReplicateNumber(repl); + brs.setHighestSection(highestSec); + return brs; + } + + @Test + void testValidateBioRisks() { + final UCMap knownBioRisks = new UCMap<>(2); + Zip.enumerate(Stream.of("BR1", "BR2")).forEach((i,s) -> knownBioRisks.put(s, new BioRisk(i+1, s))); + when(mockBioRiskService.loadAndValidateBioRisks(any(), any(), any(), any())).then(invocation -> { + Collection problems = invocation.getArgument(0); + Stream stream = invocation.getArgument(1); + Function getter = invocation.getArgument(2); + BiConsumer setter = invocation.getArgument(3); + UCMap brMap = new UCMap<>(); + stream.forEach(brs -> { + String bioRiskCode = getter.apply(brs); + if (nullOrEmpty(bioRiskCode)) { + problems.add("Missing bio risk code."); + setter.accept(brs, null); + } else { + BioRisk br = knownBioRisks.get(bioRiskCode); + if (br == null) { + problems.add("Unknown bio risk code: "+bioRiskCode); + } else { + brMap.put(br.getCode(), br); + } + } + }); + return brMap; + }); + + List brss = Stream.of(null, "", "BR1", "BR2", "BR404") + .map(TestBlockRegisterValidation::brsForBioRisk).toList(); + BlockRegisterRequest request = toRequest(brss); + var val = makeVal(request); + val.validateBioRisks(); + verify(mockBioRiskService).loadAndValidateBioRisks(any(), any(), any(), any()); + assertThat(val.getProblems()).containsExactlyInAnyOrder("Missing bio risk code.", "Unknown bio risk code: BR404"); + assertNull(brss.get(1).getBioRiskCode()); // Empty string has been replaced with null + assertEquals("BR1", brss.get(2).getBioRiskCode()); + Stream.of("BR1", "BR2").forEach(s -> assertSame(knownBioRisks.get(s), val.getBioRisk(s))); + } + + static BlockRegisterSample brsForBioRisk(String bioRiskCode) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setBioRiskCode(bioRiskCode); + return brs; + } + + @Test + void testValidateCellClasses() { + List brss = Stream.of(null, "cc1", "cc404") + .map(TestBlockRegisterValidation::brsForCellClass).toList(); + CellClass cc = new CellClass(1, "CC1", false, true); + when(mockCellClassRepo.findMapByNameIn(any())).thenReturn(UCMap.from(CellClass::getName, cc)); + BlockRegisterRequest request = toRequest(brss); + var val = makeVal(request); + val.validateCellClasses(); + verify(mockCellClassRepo).findMapByNameIn(Set.of("cc1", "cc404")); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Missing cell class name.", + "Unknown cell class name: [\"cc404\"]" + ); + assertSame(cc, val.getCellClass("cc1")); + } + + static BlockRegisterSample brsForCellClass(String cellClass) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setCellClass(cellClass); + return brs; + } + + @ParameterizedTest + @CsvSource({ + "false,false", + "true,false", + "true,true", + }) + void testValidateWorks(boolean anyWorks, boolean anyInvalid) { + BlockRegisterRequest request = new BlockRegisterRequest(); + List works; + String problem; + if (anyWorks) { + request.setWorkNumbers(List.of("SGP1")); + works = List.of(EntityFactory.makeWork("SGP1")); + problem = anyInvalid ? "Bad work" : null; + mayAddProblem(problem, UCMap.from(works, Work::getWorkNumber)) + .when(mockWorkService).validateUsableWorks(any(), any()); + } else { + works = null; + problem = "No work number supplied."; + } + var val = makeVal(request); + val.validateWorks(); + assertProblem(val.getProblems(), problem); + if (anyWorks) { + verify(mockWorkService).validateUsableWorks(any(), eq(request.getWorkNumbers())); + assertThat(val.getWorks()).containsExactlyInAnyOrderElementsOf(works); + } else { + verifyNoInteractions(mockWorkService); + assertThat(val.getWorks()).isEmpty(); + } + } + + @Test + void testValidateExternalBarcodes() { + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(Stream.of(null, "XB1", "xb1", "XBX", "LWX", "XB!") + .map(TestBlockRegisterValidation::brlForXb).toList()); + mockStringValidator(mockExternalBarcodeValidator, "external barcode"); + when(mockLwRepo.findBarcodesByBarcodeIn(any())).thenReturn(Set.of("LWX")); + when(mockLwRepo.findExternalBarcodesIn(any())).thenReturn(Set.of("XBX")); + var val = makeVal(request); + val.validateExternalBarcodes(); + Stream.of("XB1", "XBX", "LWX", "XB!") + .forEach(s -> verify(mockExternalBarcodeValidator).validate(eq(s), any())); + verifyNoMoreInteractions(mockExternalBarcodeValidator); + verify(mockLwRepo).findBarcodesByBarcodeIn(any()); + verify(mockLwRepo).findExternalBarcodesIn(Set.of("XB1", "XBX", "XB!")); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "External barcode given multiple times: xb1", + "Labware barcode already in use: [LWX]", + "External barcode already in use: [XBX]", + "Bad external barcode: XB!", + "Missing external barcode." + ); + } + + static BlockRegisterLabware brlForXb(String xb) { + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setExternalBarcode(xb); + return brl; + } + + @Test + void testValidateAddresses() { + final Address A1 = new Address(1,1), A2 = new Address(1,2), A3 = new Address(1,3); + LabwareType lt = new LabwareType(1, "lt", 1, 1, null, false); + BlockRegisterLabware brl1 = new BlockRegisterLabware(); + brl1.setLabwareType("LT"); + brl1.setSamples(List.of( + brsForAddresses(A1, A2), brsForAddresses(A1, A3), new BlockRegisterSample() + )); + BlockRegisterLabware brl2 = new BlockRegisterLabware(); + brl2.setLabwareType("???"); + brl2.setSamples(List.of(brsForAddresses(A1, A2, A3))); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl1, brl2)); + var val = makeVal(request); + val.labwareTypeMap.put(lt.getName(), lt); + + val.validateAddresses(); + assertThat(val.getProblems()).containsExactlyInAnyOrder( + "Slot addresses missing from request.", + "Invalid slot addresses for labware type lt: [A2, A3]" + ); + } + + static BlockRegisterSample brsForAddresses(Address... addresses) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setAddresses(List.of(addresses)); + return brs; + } + + static void mockStringValidator(Validator validator, String name) { + doAnswer(invocation -> { + String string = invocation.getArgument(0); + if (string.indexOf('!') < 0) { + return true; + } + Consumer problemConsumer = invocation.getArgument(1); + problemConsumer.accept("Bad "+name+": "+string); + return false; + }).when(validator).validate(any(), any()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestFileRegisterService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestFileRegisterService.java index 87d369a36..bc9790166 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestFileRegisterService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestFileRegisterService.java @@ -11,13 +11,13 @@ import uk.ac.sanger.sccp.stan.model.User; import uk.ac.sanger.sccp.stan.request.register.*; import uk.ac.sanger.sccp.stan.service.register.filereader.MultipartFileReader; +import uk.ac.sanger.sccp.utils.Zip; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; -import java.util.stream.IntStream; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -32,13 +32,13 @@ class TestFileRegisterService { @Mock private IRegisterService mockSectionRegisterService; @Mock - private IRegisterService mockBlockRegisterService; + private IRegisterService mockBlockRegisterService; @Mock private IRegisterService mockOriginalRegisterService; @Mock private MultipartFileReader mockSectionFileReader; @Mock - private MultipartFileReader mockBlockFileReader; + private MultipartFileReader mockBlockFileReader; @Mock private MultipartFileReader mockOriginalFileReader; @Mock @@ -52,7 +52,7 @@ class TestFileRegisterService { @BeforeEach void setup() { mocking = MockitoAnnotations.openMocks(this); - service = spy(new FileRegisterServiceImp(mockSectionRegisterService, mockBlockRegisterService,mockOriginalRegisterService, + service = spy(new FileRegisterServiceImp(mockSectionRegisterService, mockBlockRegisterService, mockOriginalRegisterService, mockSectionFileReader, mockBlockFileReader, mockOriginalFileReader, mockTransactor)); user = EntityFactory.getUser(); } @@ -93,13 +93,13 @@ void testSectionRegister_success(Object request, List existingExt, List< verify(fileReader).read(file); //noinspection unchecked,rawtypes verify((IRegisterService) regService).register(user, request); - if (request instanceof RegisterRequest && existingExt!=null) { - verify(service).updateWithExisting((RegisterRequest) request, existingExt); + if (request instanceof BlockRegisterRequest && existingExt!=null) { + verify(service).updateWithExisting((BlockRegisterRequest) request, existingExt); } else { verify(service, never()).updateWithExisting(any(), any()); } - if (request instanceof RegisterRequest && ignoreExt!=null) { - verify(service).updateToRemove((RegisterRequest) request, ignoreExt); + if (request instanceof BlockRegisterRequest && ignoreExt!=null) { + verify(service).updateToRemove((BlockRegisterRequest) request, ignoreExt); } else { verify(service, never()).updateToRemove(any(), any()); } @@ -110,13 +110,13 @@ static Stream regExtNamesIgnoreArgs() { return Arrays.stream(new Object[][] { {new SectionRegisterRequest()}, {new OriginalSampleRegisterRequest()}, - {new RegisterRequest()}, - {new RegisterRequest(), List.of("Alpha1"), null}, - {new RegisterRequest(), null, List.of("Beta")}, - {new RegisterRequest(), List.of("Alpha1"), List.of("Beta")}, + {new BlockRegisterRequest()}, + {new BlockRegisterRequest(), List.of("Alpha1"), null}, + {new BlockRegisterRequest(), null, List.of("Beta")}, + {new BlockRegisterRequest(), List.of("Alpha1"), List.of("Beta")}, }).map(arr -> arr.length < 3 ? Arrays.copyOf(arr, 3) : arr) .map(Arguments::of); -} + } @ParameterizedTest @MethodSource("regArgs") @@ -149,7 +149,7 @@ void testSectionRegister_IOException(Object request) throws IOException { static Stream regArgs() { return Arrays.stream(new Object[][] { {new SectionRegisterRequest()}, - {new RegisterRequest()}, + {new BlockRegisterRequest()}, {new OriginalSampleRegisterRequest()}, }).map(Arguments::of); } @@ -158,34 +158,43 @@ static Stream regArgs() { @ValueSource(booleans={false,true}) public void testUpdateWithExisting(boolean any) { String[] extNames = { null, "Alpha1", "Beta", "Alpha2" }; - List blocks = Arrays.stream(extNames) - .map(TestFileRegisterService::blockRegWithExternalName) - .toList(); - RegisterRequest request = new RegisterRequest(blocks); + BlockRegisterRequest request = requestWithExternalNames(extNames); List existing = any ? List.of("ALPHA1", "alpha2") : List.of(); service.updateWithExisting(request, existing); - IntStream.range(0, blocks.size()).forEach(i -> - assertEquals(any && (i==1 || i==3), blocks.get(i).isExistingTissue()) - ); + Zip.enumerate(streamSamples(request)).forEach((i, brs) -> + assertEquals(any && (i==1 || i==3), brs.isExistingTissue())); } @ParameterizedTest @ValueSource(booleans={false,true}) public void testUpdateToRemove(boolean anyToRemove) { String[] extNames = { null, "Alpha1", "Beta", "Alpha2" }; - RegisterRequest request = new RegisterRequest(Arrays.stream(extNames) - .map(TestFileRegisterService::blockRegWithExternalName) - .toList()); + BlockRegisterRequest request = requestWithExternalNames(extNames); List ignore = anyToRemove ? List.of("ALPHA1", "alpha2") : List.of(); service.updateToRemove(request, ignore); String[] remaining = (anyToRemove ? new String[]{null, "Beta"} : extNames); - assertThat(request.getBlocks().stream().map(BlockRegisterRequest::getExternalIdentifier)).containsExactly(remaining); + assertThat(streamSamples(request).map(BlockRegisterSample::getExternalIdentifier)).containsExactly(remaining); + } + + private static BlockRegisterRequest requestWithExternalNames(String... xns) { + List brss = Arrays.stream(xns) + .map(TestFileRegisterService::brsWithExternalName) + .toList(); + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setSamples(brss); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + return request; + } + + private static Stream streamSamples(BlockRegisterRequest request) { + return request.getLabware().stream().flatMap(brl -> brl.getSamples().stream()); } - private static BlockRegisterRequest blockRegWithExternalName(String xn) { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExternalIdentifier(xn); - return br; + private static BlockRegisterSample brsWithExternalName(String xn) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier(xn); + return brs; } } \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterClashChecker.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterClashChecker.java index 36eb65c64..4116892e2 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterClashChecker.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterClashChecker.java @@ -86,10 +86,11 @@ private Operation[] createOps() { @ParameterizedTest @MethodSource("findClashesArgs") - public void testFindClashes(RegisterRequest request, List tissues) { - Set newXns = request.getBlocks().stream() + public void testFindClashes(BlockRegisterRequest request, List tissues) { + Set newXns = request.getLabware().stream() + .flatMap(brl -> brl.getSamples().stream()) .filter(b -> !b.isExistingTissue()) - .map(BlockRegisterRequest::getExternalIdentifier) + .map(BlockRegisterSample::getExternalIdentifier) .collect(toSet()); if (newXns.isEmpty()) { assertThat(checker.findClashes(request)).isEmpty(); @@ -126,17 +127,19 @@ static Stream findClashesArgs() { ); } - static RegisterRequest makeRequest(Object... data) { - List brs = new ArrayList<>(data.length/2); - for (int i = 0; i < data.length; i += 2) { - String xn = (String) data[i]; - boolean exists = (boolean) data[i+1]; - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExternalIdentifier(xn); - br.setExistingTissue(exists); - brs.add(br); + static BlockRegisterRequest makeRequest(Object... args) { + List brss = new ArrayList<>(args.length/2); + for (int i = 0; i < args.length; i += 2) { + BlockRegisterSample brs = new BlockRegisterSample(); + brs.setExternalIdentifier((String) args[i]); + brs.setExistingTissue((Boolean) args[i+1]); + brss.add(brs); } - return new RegisterRequest(brs); + BlockRegisterLabware brl = new BlockRegisterLabware(); + brl.setSamples(brss); + BlockRegisterRequest request = new BlockRegisterRequest(); + request.setLabware(List.of(brl)); + return request; } @Test @@ -193,6 +196,5 @@ public void testToRegisterClash() { Map> tissueIdLabwareMap = Map.of(tissues[0].getId(), Arrays.asList(labware)); assertEquals(new RegisterClash(tissues[0], Arrays.asList(labware)), checker.toRegisterClash(tissues[0], tissueIdLabwareMap)); assertEquals(new RegisterClash(tissues[1], List.of()), checker.toRegisterClash(tissues[1], tissueIdLabwareMap)); - } } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java deleted file mode 100644 index a1e24819a..000000000 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java +++ /dev/null @@ -1,559 +0,0 @@ -package uk.ac.sanger.sccp.stan.service.register; - -import org.junit.jupiter.api.*; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import uk.ac.sanger.sccp.stan.EntityFactory; -import uk.ac.sanger.sccp.stan.Matchers; -import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.*; -import uk.ac.sanger.sccp.stan.service.*; -import uk.ac.sanger.sccp.stan.service.work.WorkService; - -import javax.persistence.EntityManager; -import java.time.LocalDate; -import java.util.*; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; -import static uk.ac.sanger.sccp.stan.Matchers.eqCi; - -/** - * Test {@link RegisterServiceImp} - * @author dr6 - */ -public class TestRegisterService { - @Mock - private EntityManager mockEntityManager; - @Mock - private RegisterValidationFactory mockValidationFactory; - @Mock - private DonorRepo mockDonorRepo; - @Mock - private TissueRepo mockTissueRepo; - @Mock - private SampleRepo mockSampleRepo; - @Mock - private SlotRepo mockSlotRepo; - @Mock - private BioRiskRepo mockBioRiskRepo; - @Mock - private OperationTypeRepo mockOpTypeRepo; - @Mock - private LabwareService mockLabwareService; - @Mock - private OperationService mockOpService; - @Mock - private RegisterValidation mockValidation; - @Mock - private RegisterClashChecker mockClashChecker; - @Mock - private WorkService mockWorkService; - - private User user; - private OperationType opType; - private int idCounter = 1000; - - private RegisterServiceImp registerService; - - private AutoCloseable mocking; - - @BeforeEach - void setup() { - mocking = MockitoAnnotations.openMocks(this); - user = EntityFactory.getUser(); - when(mockValidationFactory.createRegisterValidation(any())).thenReturn(mockValidation); - BioState bs = EntityFactory.getBioState(); - opType = new OperationType(1, "Register", 0, bs); - when(mockOpTypeRepo.getByName(opType.getName())).thenReturn(opType); - - registerService = spy(new RegisterServiceImp(mockEntityManager, mockValidationFactory, mockDonorRepo, mockTissueRepo, - mockSampleRepo, mockSlotRepo, mockBioRiskRepo, mockOpTypeRepo, mockLabwareService, mockOpService, mockWorkService, mockClashChecker)); - } - - @AfterEach - void tearDown() throws Exception { - mocking.close(); - } - - @Test - public void testRegisterNoBlocks() { - RegisterResult result = registerService.register(user, new RegisterRequest(List.of())); - assertThat(result.getLabware()).isEmpty(); - verifyNoInteractions(mockValidationFactory); - verify(registerService, never()).updateExistingTissues(any(), any()); - verify(registerService, never()).create(any(), any(), any()); - } - - @Test - public void testRegisterValidBlocks() { - RegisterRequest request = new RegisterRequest(List.of(new BlockRegisterRequest())); - when(mockValidation.validate()).thenReturn(Set.of()); - final RegisterResult result = new RegisterResult(List.of(EntityFactory.getTube())); - doNothing().when(registerService).updateExistingTissues(any(), any()); - doReturn(result).when(registerService).create(any(), any(), any()); - when(mockClashChecker.findClashes(any())).thenReturn(List.of()); - - assertSame(result, registerService.register(user, request)); - - verify(mockClashChecker).findClashes(request); - verify(mockValidationFactory).createRegisterValidation(request); - verify(mockValidation).validate(); - verify(registerService).updateExistingTissues(request, mockValidation); - verify(registerService).create(request, user, mockValidation); - } - - @Test - public void testRegisterWithClashes() { - RegisterRequest request = new RegisterRequest(List.of(new BlockRegisterRequest())); - List clashes = List.of(new RegisterClash(EntityFactory.getTissue(), List.of())); - when(mockClashChecker.findClashes(any())).thenReturn(clashes); - assertEquals(RegisterResult.clashes(clashes), registerService.register(user, request)); - verifyNoInteractions(mockValidationFactory); - verifyNoInteractions(mockValidation); - verify(registerService, never()).updateExistingTissues(any(), any()); - verify(registerService, never()).create(any(), any(), any()); - } - - @Test - public void testRegisterInvalidBlocks() { - RegisterRequest request = new RegisterRequest(List.of(new BlockRegisterRequest())); - final Set problems = Set.of("Everything is bad.", "I spilled my tea."); - when(mockValidation.validate()).thenReturn(problems); - - try { - registerService.register(user, request); - fail("Expected validation exception."); - } catch (ValidationException ex) { - assertEquals(ex.getProblems(), problems); - } - - verify(mockValidationFactory).createRegisterValidation(request); - verify(mockValidation).validate(); - verify(registerService, never()).updateExistingTissues(any(), any()); - verify(registerService, never()).create(any(), any(), any()); - } - - @Test - public void testCreateDonors() { - Donor donor0 = EntityFactory.getDonor(); - Species hamster = new Species(2, "Hamster"); - Donor donor1 = new Donor(null, "Jeff", LifeStage.paediatric, hamster); - BlockRegisterRequest block0 = new BlockRegisterRequest(); - block0.setDonorIdentifier(donor0.getDonorName()); - block0.setLifeStage(donor0.getLifeStage()); - block0.setSpecies(donor0.getSpecies().getName()); - BlockRegisterRequest block1 = new BlockRegisterRequest(); - block1.setDonorIdentifier(donor1.getDonorName()); - block1.setLifeStage(donor1.getLifeStage()); - block1.setSpecies(donor1.getSpecies().getName()); - - when(mockValidation.getDonor(eqCi(donor0.getDonorName()))).thenReturn(donor0); - when(mockValidation.getDonor(eqCi(donor1.getDonorName()))).thenReturn(donor1); - - when(mockDonorRepo.save(any())).then(invocation -> { - Donor donor = invocation.getArgument(0); - assertNull(donor.getId()); - donor.setId(++idCounter); - return donor; - }); - - RegisterRequest request = new RegisterRequest(List.of(block0, block1)); - Map donorMap = registerService.createDonors(request, mockValidation); - assertEquals(donorMap, Stream.of(donor0, donor1).collect(toMap(d -> d.getDonorName().toUpperCase(), d -> d))); - verify(mockDonorRepo).save(donor1); - verifyNoMoreInteractions(mockDonorRepo); - } - - @Test - public void testUpdateExistingTissues_none() { - Tissue tissue1 = EntityFactory.getTissue(); - BlockRegisterRequest brr1 = new BlockRegisterRequest(); - brr1.setExternalIdentifier(tissue1.getExternalName()); - brr1.setExistingTissue(true); - - Tissue tissue2 = EntityFactory.makeTissue(tissue1.getDonor(), tissue1.getSpatialLocation()); - tissue2.setCollectionDate(LocalDate.of(2020,1,2)); - BlockRegisterRequest brr2 = new BlockRegisterRequest(); - brr2.setExternalIdentifier(tissue2.getExternalName().toLowerCase()); - brr2.setExistingTissue(true); - brr2.setSampleCollectionDate(tissue2.getCollectionDate()); - - BlockRegisterRequest brr3 = new BlockRegisterRequest(); - - when(mockValidation.getTissue(Matchers.eqCi(tissue1.getExternalName()))).thenReturn(tissue1); - when(mockValidation.getTissue(Matchers.eqCi(tissue2.getExternalName()))).thenReturn(tissue2); - - registerService.updateExistingTissues(new RegisterRequest(List.of(brr1, brr2, brr3)), mockValidation); - verifyNoInteractions(mockTissueRepo); - } - - @Test - public void testUpdateExistingTissues() { - Donor donor = EntityFactory.getDonor(); - SpatialLocation sl = EntityFactory.getSpatialLocation(); - Tissue tissue = EntityFactory.makeTissue(donor, sl); - - when(mockValidation.getTissue(eqCi(tissue.getExternalName()))).thenReturn(tissue); - BlockRegisterRequest brr1 = new BlockRegisterRequest(); - brr1.setExistingTissue(true); - brr1.setExternalIdentifier(tissue.getExternalName().toLowerCase()); - brr1.setSampleCollectionDate(LocalDate.of(2010,2,3)); - - registerService.updateExistingTissues(new RegisterRequest(List.of(brr1, new BlockRegisterRequest())), mockValidation); - verify(mockTissueRepo).saveAll(List.of(tissue)); - assertEquals(brr1.getSampleCollectionDate(), tissue.getCollectionDate()); - } - - @Test - public void testCreateTissues() { - Tissue existingTissue = EntityFactory.getTissue(); - Species human = EntityFactory.getHuman(); - Species hamster = new Species(2, "Hamster"); - Donor donor1 = existingTissue.getDonor(); - Donor donor2 = new Donor(2, "DONOR2", LifeStage.adult, human); - Donor donor3 = new Donor(3, "DONOR3", LifeStage.fetal, hamster); - Map donorMap = Stream.of(donor1, donor2, donor3) - .collect(toMap(d -> d.getDonorName().toUpperCase(), d -> d)); - doReturn(donorMap).when(registerService).createDonors(any(), any()); - when(mockTissueRepo.save(any())).then(invocation -> { - Tissue tissue = invocation.getArgument(0); - assertNull(tissue.getId()); - tissue.setId(++idCounter); - return tissue; - }); - Hmdmc hmdmc = EntityFactory.getHmdmc(); - SpatialLocation sl = EntityFactory.getSpatialLocation(); - Medium medium = EntityFactory.getMedium(); - Fixative fix = EntityFactory.getFixative(); - - when(mockValidation.getTissue(existingTissue.getExternalName().toUpperCase())).thenReturn(existingTissue); - when(mockValidation.getHmdmc(hmdmc.getHmdmc())).thenReturn(hmdmc); - when(mockValidation.getSpatialLocation(sl.getTissueType().getName(), sl.getCode())).thenReturn(sl); - when(mockValidation.getMedium(medium.getName())).thenReturn(medium); - when(mockValidation.getFixative(fix.getName())).thenReturn(fix); - LocalDate colDate = LocalDate.of(2020,5,4); - List brs = List.of( - makeBrr(existingTissue.getExternalName(), donor1.getDonorName(), - existingTissue.getHmdmc().getHmdmc(), donor1.getSpecies().getName(), - existingTissue.getReplicate(), existingTissue.getSpatialLocation(), - existingTissue.getMedium().getName(), existingTissue.getFixative().getName(), null), - makeBrr("TISSUE2", donor2.getDonorName(), - hmdmc.getHmdmc(), human.getName(), - "7", sl, medium.getName(), fix.getName(), colDate), - makeBrr("TISSUE3", donor3.getDonorName(), - null, hamster.getName(), - "14", sl, medium.getName(), fix.getName(), null) - ); - - RegisterRequest request = new RegisterRequest(brs); - - Map tissueMap = registerService.createTissues(request, mockValidation); - - assertThat(tissueMap).hasSize(3); - assertEquals(3L, tissueMap.values().stream().map(Tissue::getId).distinct().count()); - - assertSame(existingTissue, tissueMap.get(existingTissue.getExternalName().toUpperCase())); - - for (String xn : new String[] {"TISSUE2", "TISSUE3"}) { - Tissue tissue = tissueMap.get(xn); - assertNotNull(tissue); - assertEquals(xn, tissue.getExternalName()); - if (xn.equals("TISSUE2")) { - assertEquals(donor2, tissue.getDonor()); - assertEquals(hmdmc, tissue.getHmdmc()); - assertEquals("7", tissue.getReplicate()); - assertEquals(colDate, tissue.getCollectionDate()); - } else { - assertEquals(donor3, tissue.getDonor()); - assertNull(tissue.getHmdmc()); - assertEquals("14", tissue.getReplicate()); - assertNull(tissue.getCollectionDate()); - } - assertEquals(sl, tissue.getSpatialLocation()); - assertEquals(medium, tissue.getMedium()); - assertEquals(fix, tissue.getFixative()); - - verify(mockTissueRepo).save(tissue); - } - } - - private BlockRegisterRequest makeBrr(String externalName, String donorName, - String hmdmc, String species, - String replicate, SpatialLocation sl, - String mediumName, String fixName, LocalDate collectionDate) { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExternalIdentifier(externalName); - br.setDonorIdentifier(donorName); - br.setHmdmc(hmdmc); - br.setSpecies(species); - br.setReplicateNumber(replicate); - br.setTissueType(sl.getTissueType().getName()); - br.setSpatialLocation(sl.getCode()); - br.setMedium(mediumName); - br.setFixative(fixName); - br.setSampleCollectionDate(collectionDate); - return br; - } - - // This test does not mock out createTissues() so it is actually testing more thoroughly than it needs to - @Test - public void testCreate() { - Species hamster = new Species(2, "Hamster"); - Donor donor1 = EntityFactory.getDonor(); - Donor donor2 = new Donor(donor1.getId()+1, "DONOR2", LifeStage.adult, hamster); - LabwareType[] lts = {EntityFactory.getTubeType(), EntityFactory.makeLabwareType(1, 2)}; - TissueType tissueType = EntityFactory.getTissueType(); - Medium medium = EntityFactory.getMedium(); - Fixative fixative = EntityFactory.getFixative(); - CellClass cellClass = EntityFactory.getCellClass(); - Hmdmc[] hmdmcs = {new Hmdmc(20000, "20/000"), new Hmdmc(20001, "20/001")}; - - BlockRegisterRequest block0 = new BlockRegisterRequest(); - block0.setDonorIdentifier(donor1.getDonorName()); - block0.setLifeStage(donor1.getLifeStage()); - block0.setExternalIdentifier("TISSUE0"); - block0.setHighestSection(3); - block0.setHmdmc("20/000"); - block0.setLabwareType(lts[0].getName()); - block0.setMedium(medium.getName()); - block0.setFixative(fixative.getName()); - block0.setTissueType(tissueType.getName()); - block0.setReplicateNumber("2"); - block0.setSpatialLocation(1); - block0.setSpecies(donor1.getSpecies().getName()); - block0.setCellClass("Tissue"); - - BlockRegisterRequest block1 = new BlockRegisterRequest(); - block1.setDonorIdentifier(donor2.getDonorName()); - block1.setLifeStage(donor2.getLifeStage()); - block1.setReplicateNumber("5"); - block1.setTissueType(tissueType.getName()); - block1.setSpatialLocation(0); - block1.setLabwareType(lts[1].getName()); - block1.setMedium(medium.getName().toUpperCase()); - block1.setFixative(fixative.getName().toUpperCase()); - block1.setHighestSection(0); - block1.setExternalIdentifier("TISSUE1"); - block1.setSpecies(donor2.getSpecies().getName()); - block1.setCellClass("Tissue"); - block1.setSampleCollectionDate(LocalDate.of(2020,4,6)); - - Map donorMap = Map.of(donor1.getDonorName().toUpperCase(), donor1, - donor2.getDonorName().toUpperCase(), donor2); - doReturn(donorMap).when(registerService).createDonors(any(), any()); - SpatialLocation[] sls = {new SpatialLocation(1, "SL0", 1, tissueType), - new SpatialLocation(2, "SL1", 0, tissueType)}; - - List works = IntStream.rangeClosed(1,2) - .mapToObj(i -> { - Work work = new Work(); - work.setId(i); - work.setWorkNumber("SGP"+i); - return work; - }).collect(toList()); - when(mockValidation.getWorks()).thenReturn(works); - - when(mockValidation.getSpatialLocation(eqCi(tissueType.getName()), eq(1))) - .thenReturn(sls[0]); - when(mockValidation.getSpatialLocation(eqCi(tissueType.getName()), eq(0))) - .thenReturn(sls[1]); - when(mockValidation.getMedium(eqCi(medium.getName()))).thenReturn(medium); - when(mockValidation.getFixative(eqCi(fixative.getName()))).thenReturn(fixative); - when(mockValidation.getCellClass("Tissue")).thenReturn(cellClass); - Arrays.stream(lts).forEach(lt -> when(mockValidation.getLabwareType(eqCi(lt.getName()))).thenReturn(lt)); - Arrays.stream(hmdmcs).forEach(h -> when(mockValidation.getHmdmc(eqCi(h.getHmdmc()))).thenReturn(h)); - RegisterRequest request = new RegisterRequest(List.of(block0, block1)); - Labware[] lws = Arrays.stream(lts).map(EntityFactory::makeEmptyLabware).toArray(Labware[]::new); - Arrays.stream(lws).forEach(lw -> when(mockLabwareService.create(lw.getLabwareType())).thenReturn(lw)); - - Tissue[] tissues = new Tissue[]{ - new Tissue(5000, block0.getExternalIdentifier(), block0.getReplicateNumber(), - sls[0], donor1, medium, fixative, cellClass, hmdmcs[0], block0.getSampleCollectionDate(), null), - new Tissue(5001, block1.getExternalIdentifier(), block1.getReplicateNumber(), - sls[1], donor2, medium, fixative, cellClass, null, block1.getSampleCollectionDate(), null), - }; - - BioState bioState = opType.getNewBioState(); - Sample[] samples = new Sample[]{ - new Sample(6000, null, tissues[0], bioState), - new Sample(6001, null, tissues[1], bioState), - }; - - when(mockTissueRepo.save(any())).thenReturn(tissues[0], tissues[1]); - when(mockSampleRepo.save(any())).thenReturn(samples[0], samples[1]); - - when(mockSlotRepo.save(any())).then(invocation -> invocation.getArgument(0)); - - List ops = IntStream.rangeClosed(1,request.getBlocks().size()) - .mapToObj(i -> { - Operation op = new Operation(); - op.setId(i); - return op; - }).collect(toList()); - - when(mockOpService.createOperationInPlace(any(), any(), any(), any())).thenReturn(ops.get(0), ops.get(1)); - - RegisterResult result = registerService.create(request, user, mockValidation); - - assertEquals(result, new RegisterResult(Arrays.asList(lws))); - - verify(registerService).createDonors(request, mockValidation); - - List blocks = request.getBlocks(); - for (int i = 0; i < blocks.size(); i++) { - BlockRegisterRequest block = blocks.get(i); - verify(mockTissueRepo).save( - new Tissue(null, - block.getExternalIdentifier(), - block.getReplicateNumber(), - sls[i], - i==0 ? donor1 : donor2, - medium, - fixative, - cellClass, - i==0 ? hmdmcs[i] : null, - block.getSampleCollectionDate(), null)); - verify(mockSampleRepo).save(new Sample(null, null, tissues[i], bioState)); - verify(mockLabwareService).create(lts[i]); - Labware lw = lws[i]; - verify(mockEntityManager).refresh(lw); - Slot slot = lw.getFirstSlot(); - assertEquals(block.getHighestSection(), slot.getBlockHighestSection()); - assertEquals(samples[i].getId(), slot.getBlockSampleId()); - verify(mockSlotRepo).save(slot); - verify(mockOpService).createOperationInPlace(opType, user, slot, samples[i]); - } - verify(mockWorkService).link(works, ops); - } - - @ParameterizedTest - @MethodSource("createArgs") - public void testCreateProblems(Species species, Object hmdmcObj, String expectedErrorMessage) { - Donor donor = new Donor(100, "DONOR1", LifeStage.adult, species); - LabwareType lt = EntityFactory.getTubeType(); - TissueType tissueType = EntityFactory.getTissueType(); - Medium medium = EntityFactory.getMedium(); - Fixative fixative = EntityFactory.getFixative(); - CellClass cellClass = EntityFactory.getCellClass(); - Hmdmc hmdmc; - String hmdmcString; - if (hmdmcObj instanceof Hmdmc) { - hmdmc = (Hmdmc) hmdmcObj; - hmdmcString = hmdmc.getHmdmc(); - } else if (hmdmcObj instanceof String) { - hmdmc = null; - hmdmcString = (String) hmdmcObj; - } else { - hmdmc = null; - hmdmcString = null; - } - BioRisk br = new BioRisk(800, "biorisk"); - - BlockRegisterRequest block = new BlockRegisterRequest(); - block.setDonorIdentifier(donor.getDonorName()); - block.setLifeStage(donor.getLifeStage()); - block.setExternalIdentifier("TISSUE"); - block.setHighestSection(3); - block.setHmdmc(hmdmcString); - block.setLabwareType(lt.getName()); - block.setMedium(medium.getName()); - block.setFixative(fixative.getName()); - block.setCellClass(cellClass.getName()); - block.setTissueType(tissueType.getName()); - block.setReplicateNumber("2"); - block.setSpatialLocation(1); - block.setSpecies(species.getName()); - block.setBioRiskCode(br.getCode()); - - Map donorMap = Map.of(donor.getDonorName().toUpperCase(), donor); - doReturn(donorMap).when(registerService).createDonors(any(), any()); - final SpatialLocation sl = new SpatialLocation(1, "SL0", 1, tissueType); - - Operation op = new Operation(); - op.setId(700); - when(mockOpService.createOperationInPlace(any(), any(), any(), any())).thenReturn(op); - - when(mockValidation.getBioRisk(br.getCode())).thenReturn(br); - - when(mockValidation.getSpatialLocation(eqCi(tissueType.getName()), eq(1))) - .thenReturn(sl); - when(mockValidation.getMedium(eqCi(medium.getName()))).thenReturn(medium); - when(mockValidation.getFixative(eqCi(fixative.getName()))).thenReturn(fixative); - when(mockValidation.getLabwareType(eqCi(lt.getName()))).thenReturn(lt); - when(mockValidation.getCellClass(eq(cellClass.getName()))).thenReturn(cellClass); - if (hmdmc != null) { - when(mockValidation.getHmdmc(hmdmc.getHmdmc())).thenReturn(hmdmc); - } else if (hmdmcString!=null) { - when(mockValidation.getHmdmc(hmdmcString)).thenReturn(null); - } - RegisterRequest request = new RegisterRequest(List.of(block)); - Labware lw = EntityFactory.makeEmptyLabware(lt); - when(mockLabwareService.create(lt)).thenReturn(lw); - - final Tissue tissue = new Tissue(5000, block.getExternalIdentifier(), block.getReplicateNumber(), - sl, donor, medium, fixative, null, hmdmc, null, null); - - BioState bioState = opType.getNewBioState(); - Sample sample = new Sample(6000, null, tissue, bioState); - - when(mockTissueRepo.save(any())).thenReturn(tissue); - when(mockSampleRepo.save(any())).thenReturn(sample); - - when(mockSlotRepo.save(any())).then(invocation -> invocation.getArgument(0)); - - if (expectedErrorMessage!=null) { - assertThat(assertThrows(IllegalArgumentException.class, () -> registerService.create(request, user, mockValidation))) - .hasMessage(expectedErrorMessage); - return; - } else { - registerService.create(request, user, mockValidation); - } - - verify(registerService).createDonors(request, mockValidation); - - verify(mockTissueRepo).save( - new Tissue(null, - block.getExternalIdentifier(), - block.getReplicateNumber(), - sl, - donor, - medium, - fixative, - cellClass, - hmdmc, - null, null)); - verify(mockSampleRepo).save(new Sample(null, null, tissue, bioState)); - verify(mockLabwareService).create(lt); - verify(mockEntityManager).refresh(lw); - Slot slot = lw.getFirstSlot(); - assertEquals(slot.getBlockHighestSection(), block.getHighestSection()); - assertEquals(slot.getBlockSampleId(), sample.getId()); - verify(mockSlotRepo).save(slot); - verify(mockOpService).createOperationInPlace(opType, user, slot, sample); - verify(mockBioRiskRepo).recordBioRisk(sample, br, op.getId()); - } - - static Stream createArgs() { - Species human = new Species(1, Species.HUMAN_NAME); - Species hamster = new Species(2, "Hamster"); - Hmdmc hmdmc = new Hmdmc(10, "20/001"); - // Species species, Hmdmc hmdmc, String expectedErrorMessage - return Stream.of( - Arguments.of(human, hmdmc, null), - Arguments.of(hamster, null, null), - Arguments.of(human, null, "No HuMFre number given for tissue TISSUE"), - Arguments.of(hamster, hmdmc, "HuMFre number given for non-human tissue TISSUE"), - Arguments.of(human, "20/404", "Unknown HuMFre number: 20/404") - ); - } -} diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java deleted file mode 100644 index 857018a42..000000000 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java +++ /dev/null @@ -1,933 +0,0 @@ -package uk.ac.sanger.sccp.stan.service.register; - -import com.google.common.base.MoreObjects; -import org.junit.jupiter.api.*; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.*; -import org.mockito.*; -import org.mockito.verification.VerificationMode; -import uk.ac.sanger.sccp.stan.EntityFactory; -import uk.ac.sanger.sccp.stan.Matchers; -import uk.ac.sanger.sccp.stan.model.*; -import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; -import uk.ac.sanger.sccp.stan.service.BioRiskService; -import uk.ac.sanger.sccp.stan.service.Validator; -import uk.ac.sanger.sccp.stan.service.register.RegisterValidationImp.StringIntKey; -import uk.ac.sanger.sccp.stan.service.work.WorkService; -import uk.ac.sanger.sccp.utils.UCMap; -import uk.ac.sanger.sccp.utils.Zip; - -import java.time.LocalDate; -import java.util.*; -import java.util.function.*; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.*; -import static uk.ac.sanger.sccp.stan.EntityFactory.objToList; -import static uk.ac.sanger.sccp.stan.Matchers.assertProblem; - -/** - * Tests for {@link RegisterValidationImp} - * @author dr6 - */ -public class TestRegisterValidation { - @Mock - private DonorRepo mockDonorRepo; - @Mock - private HmdmcRepo mockHmdmcRepo; - @Mock - private TissueTypeRepo mockTtRepo; - @Mock - private LabwareTypeRepo mockLtRepo; - @Mock - private MediumRepo mockMediumRepo; - @Mock - private FixativeRepo mockFixativeRepo; - @Mock - private TissueRepo mockTissueRepo; - @Mock - private SpeciesRepo mockSpeciesRepo; - @Mock - private CellClassRepo mockCellClassRepo; - @Mock - private Validator mockDonorNameValidation; - @Mock - private Validator mockExternalNameValidation; - @Mock - private Validator mockReplicateValidator; - @Mock - private TissueFieldChecker mockFieldChecker; - @Mock - private BioRiskService mockBioRiskService; - @Mock - private WorkService mockWorkService; - - private AutoCloseable mocking; - - @BeforeEach - void setup() { - mocking = MockitoAnnotations.openMocks(this); - } - - @AfterEach - void cleanup() throws Exception { - mocking.close(); - } - - private void loadSpecies(final Collection specieses) { - when(mockSpeciesRepo.findByName(any())).then(invocation -> { - String name = invocation.getArgument(0); - return specieses.stream().filter(sp -> sp.getName().equalsIgnoreCase(name)).findAny(); - }); - } - - private RegisterValidationImp create(RegisterRequest request) { - return spy(new RegisterValidationImp(request, mockDonorRepo, mockHmdmcRepo, mockTtRepo, mockLtRepo, - mockMediumRepo, mockFixativeRepo, mockTissueRepo, mockSpeciesRepo, mockCellClassRepo, - mockDonorNameValidation, mockExternalNameValidation, mockReplicateValidator, mockFieldChecker, - mockBioRiskService, mockWorkService)); - } - - private void stubValidationMethods(RegisterValidationImp validation) { - doNothing().when(validation).validateDonors(); - doNothing().when(validation).validateHmdmcs(); - doNothing().when(validation).validateSpatialLocations(); - doNothing().when(validation).validateLabwareTypes(); - doNothing().when(validation).validateMediums(); - doNothing().when(validation).validateExistingTissues(); - doNothing().when(validation).validateNewTissues(); - doNothing().when(validation).validateFixatives(); - doNothing().when(validation).validateCollectionDates(); - doNothing().when(validation).validateBioRisks(); - doNothing().when(validation).validateWorks(); - doNothing().when(validation).validateCellClasses(); - } - - private void verifyValidateMethods(RegisterValidationImp validation, VerificationMode verificationMode) { - verify(validation, verificationMode).validateDonors(); - verify(validation, verificationMode).validateHmdmcs(); - verify(validation, verificationMode).validateSpatialLocations(); - verify(validation, verificationMode).validateLabwareTypes(); - verify(validation, verificationMode).validateMediums(); - verify(validation, verificationMode).validateExistingTissues(); - verify(validation, verificationMode).validateNewTissues(); - verify(validation, verificationMode).validateFixatives(); - verify(validation, verificationMode).validateCollectionDates(); - verify(validation, verificationMode).validateWorks(); - verify(validation, verificationMode).validateBioRisks(); - verify(validation, verificationMode).validateCellClasses(); - } - - @Test - public void testValidateEmptyRequest() { - RegisterRequest request = new RegisterRequest(List.of()); - RegisterValidationImp validation = create(request); - stubValidationMethods(validation); - - assertThat(validation.validate()).isEmpty(); - verifyValidateMethods(validation, never()); - } - - @Test - public void testValidateNonemptyRequestWithoutProblems() { - RegisterRequest request = new RegisterRequest(List.of(new BlockRegisterRequest())); - RegisterValidationImp validation = create(request); - stubValidationMethods(validation); - assertThat(validation.validate()).isEmpty(); - verifyValidateMethods(validation, times(1)); - } - - @Test - public void testValidateNonemptyRequestWithProblems() { - RegisterRequest request = new RegisterRequest(List.of(new BlockRegisterRequest())); - RegisterValidationImp validation = create(request); - stubValidationMethods(validation); - doAnswer(invocation -> validation.problems.add("Problem alpha.")) - .when(validation).validateDonors(); - doAnswer(invocation -> validation.problems.add("Problem beta.")) - .when(validation).validateHmdmcs(); - assertThat(validation.validate()).hasSameElementsAs(List.of("Problem alpha.", "Problem beta.")); - verifyValidateMethods(validation, times(1)); - } - - @ParameterizedTest - @MethodSource("donorData") - public void testValidateDonors(List donorNames, List lifeStages, List speciesNames, - List knownDonors, List knownSpecies, List expectedDonors, - List expectedProblems) { - loadSpecies(knownSpecies); - Iterator lifeStageIter = lifeStages.listIterator(); - Iterator speciesIter = speciesNames.iterator(); - RegisterRequest request = new RegisterRequest( - donorNames.stream() - .map(donorName -> { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setDonorIdentifier(donorName); - br.setLifeStage(lifeStageIter.next()); - br.setSpecies(speciesIter.next()); - return br; - }) - .collect(toList()) - ); - when(mockDonorRepo.findByDonorName(any())).then(invocation -> { - final String name = invocation.getArgument(0); - return knownDonors.stream().filter(d -> name.equalsIgnoreCase(d.getDonorName())).findAny(); - }); - - RegisterValidationImp validation = create(request); - validation.validateDonors(); - assertThat(validation.getProblems()).hasSameElementsAs(expectedProblems); - Map expectedDonorMap = expectedDonors.stream().collect(toMap(d -> d.getDonorName().toUpperCase(), d -> d)); - assertEquals(expectedDonorMap, validation.donorMap); - expectedDonors.forEach(donor -> - assertEquals(donor, validation.getDonor(donor.getDonorName())) - ); - } - - @Test - public void testDonorNameValidation() { - when(mockDonorNameValidation.validate(any(), any())).then(invocation -> { - String name = invocation.getArgument(0); - Consumer addProblem = invocation.getArgument(1); - if (name.contains("*")) { - addProblem.accept("Invalid name: "+name); - return false; - } - return true; - }); - loadSpecies(List.of(EntityFactory.getHuman())); - RegisterRequest request = new RegisterRequest( - Stream.of("Alpha", "Beta", "Gamma*", "Delta*", "Gamma*") - .map(s -> { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setDonorIdentifier(s); - br.setLifeStage(LifeStage.adult); - br.setSpecies(Species.HUMAN_NAME); - return br; - }) - .collect(toList()) - ); - RegisterValidationImp validation = create(request); - validation.validateDonors(); - assertThat(validation.getProblems()).hasSameElementsAs(List.of("Invalid name: Gamma*", "Invalid name: Delta*")); - } - - @ParameterizedTest - @MethodSource("slData") - public void testValidateSpatialLocations(List tissueTypeNames, List codes, - List knownTissueTypes, List expectedSLs, - List expectedProblems) { - RegisterRequest request = new RegisterRequest( - Zip.of(tissueTypeNames.stream(), codes.stream()) - .map((name, code) -> { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setTissueType(name); - br.setSpatialLocation(code); - return br; - }).toList()); - when(mockTtRepo.findByName(any())).then(invocation -> { - final String arg = invocation.getArgument(0); - return knownTissueTypes.stream().filter(tt -> arg.equalsIgnoreCase(tt.getName())).findAny(); - }); - - RegisterValidationImp validation = create(request); - validation.validateSpatialLocations(); - assertThat(validation.getProblems()).hasSameElementsAs(expectedProblems); - Map expectedSLMap = expectedSLs.stream() - .collect(toMap(sl -> new StringIntKey(sl.getTissueType().getName(), sl.getCode()), sl -> sl)); - assertEquals(expectedSLMap, validation.spatialLocationMap); - expectedSLs.forEach(sl -> - assertEquals(sl, validation.getSpatialLocation(sl.getTissueType().getName(), sl.getCode())) - ); - } - - @ParameterizedTest - @MethodSource("hmdmcData") - public void testValidateHmdmcs(List knownHmdmcs, List givenHmdmcs, List speciesNames, - List expectedHmdmcs, List expectedProblems) { - when(mockHmdmcRepo.findByHmdmc(any())).then(invocation -> { - final String arg = invocation.getArgument(0); - return knownHmdmcs.stream().filter(h -> arg.equalsIgnoreCase(h.getHmdmc())).findAny(); - }); - - RegisterRequest request = new RegisterRequest( - Zip.of(givenHmdmcs.stream(), speciesNames.stream()) - .map((hmdmc, species) -> { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setHmdmc(hmdmc); - br.setSpecies(species); - return br; - }) - .toList() - ); - - RegisterValidationImp validation = create(request); - validation.validateHmdmcs(); - assertThat(validation.getProblems()).hasSameElementsAs(expectedProblems); - Map expectedItemMap = expectedHmdmcs.stream().collect(toMap(item -> item.getHmdmc().toUpperCase(), h -> h)); - Map actualMap = validation.hmdmcMap.entrySet().stream() - .filter(e -> e.getValue() != null) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); - assertEquals(expectedItemMap, actualMap); - for (Hmdmc hmdmc : expectedHmdmcs) { - assertEquals(hmdmc, validation.getHmdmc(hmdmc.getHmdmc())); - } - } - - @ParameterizedTest - @MethodSource("ltData") - public void testValidateLabwareTypes(List knownLts, List givenLtNames, - List expectedLts, List expectedProblems) { - when(mockLtRepo.findByName(any())).then(invocation -> { - final String arg = invocation.getArgument(0); - return knownLts.stream().filter(lt -> arg.equalsIgnoreCase(lt.getName())).findAny(); - }); - testValidateSimpleField(givenLtNames, expectedLts, expectedProblems, - RegisterValidationImp::validateLabwareTypes, LabwareType::getName, BlockRegisterRequest::setLabwareType, - v -> v.labwareTypeMap, RegisterValidationImp::getLabwareType); - } - - @ParameterizedTest - @MethodSource("mediumData") - public void testValidateMediums(List knownItems, List givenNames, - List expectedItems, List expectedProblems) { - when(mockMediumRepo.findByName(any())).then(invocation -> { - final String arg = invocation.getArgument(0); - return knownItems.stream().filter(item -> arg.equalsIgnoreCase(item.getName())).findAny(); - }); - testValidateSimpleField(givenNames, expectedItems, expectedProblems, - RegisterValidationImp::validateMediums, Medium::getName, BlockRegisterRequest::setMedium, - v -> v.mediumMap, RegisterValidationImp::getMedium); - } - - @ParameterizedTest - @MethodSource("fixativeData") - public void testValidateFixatives(List knownItems, List givenNames, - List expectedItems, List expectedProblems) { - when(mockFixativeRepo.findByName(any())).then(invocation -> { - final String arg = invocation.getArgument(0); - return knownItems.stream().filter(item -> arg.equalsIgnoreCase(item.getName())).findAny(); - }); - testValidateSimpleField(givenNames, expectedItems, expectedProblems, - RegisterValidationImp::validateFixatives, Fixative::getName, BlockRegisterRequest::setFixative, - v -> v.fixativeMap, RegisterValidationImp::getFixative); - } - - @ParameterizedTest - @MethodSource("newTissueData") - void testValidateNewTissues(final List testData, List expectedProblems) { - when(mockExternalNameValidation.validate(any(), any())).then(invocation -> { - String name = invocation.getArgument(0); - Consumer addProblem = invocation.getArgument(1); - if (name.contains("*")) { - addProblem.accept("Invalid name: " + name); - return true; - } - return false; - }); - when(mockTissueRepo.findAllByExternalName(anyString())).then(invocation -> { - final String name = invocation.getArgument(0); - return testData.stream() - .filter(td -> td.anyWithSameIdentifier && td.externalName.equalsIgnoreCase(name)) - .map(td -> EntityFactory.getTissue()) - .collect(toList()); - }); - when(mockReplicateValidator.validate(any(), any())).then(invocation -> { - String replicate = invocation.getArgument(0); - Consumer addProblem = invocation.getArgument(1); - if (!replicate.matches("\\d+[a-zA-Z]?")) { - addProblem.accept("Invalid replicate: " + replicate); - return true; - } - return false; - }); - - RegisterRequest request = new RegisterRequest( - testData.stream() - .map(td -> { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExternalIdentifier(td.externalName); - br.setReplicateNumber(td.replicate); - br.setDonorIdentifier(td.donorName); - br.setTissueType(td.tissueTypeName); - br.setSpatialLocation(td.slCode); - br.setMedium(td.mediumName); - br.setHighestSection(td.highestSection); - br.setFixative(td.fixativeName); - br.setExistingTissue(td.existing); - return br; - }) - .collect(toList()) - ); - - RegisterValidationImp validation = create(request); - - validation.validateNewTissues(); - assertThat(validation.getProblems()).hasSameElementsAs(expectedProblems); - } - - @ParameterizedTest - @MethodSource("validateExistingTissuesArgs") - public void testValidateExistingTissue(Object testDataObj, Object existingTissuesObj, - Object expectedProblemsObj) { - final List testData = objToList(testDataObj); - final List existingTissues = objToList(existingTissuesObj); - final List expectedProblems = objToList(expectedProblemsObj); - when(mockTissueRepo.findAllByExternalNameIn(any())).then(invocation -> { - Collection xns = invocation.getArgument(0); - return existingTissues.stream().filter(t -> xns.stream().anyMatch(xn -> t.getExternalName().equalsIgnoreCase(xn))) - .collect(toList()); - }); - List brs = new ArrayList<>(testData.size()); - for (ValidateExistingTissueTestData td : testData) { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExternalIdentifier(td.externalName); - br.setExistingTissue(td.existing); - if (td.fieldProblem != null) { - doAnswer(invocation -> { - Consumer problemConsumer = invocation.getArgument(0); - problemConsumer.accept(td.fieldProblem); - return null; - }).when(mockFieldChecker).check(any(), same(br), any()); - } - brs.add(br); - } - RegisterRequest request = new RegisterRequest(brs); - - RegisterValidationImp validation = create(request); - validation.validateExistingTissues(); - assertThat(validation.getProblems()).containsExactlyInAnyOrderElementsOf(expectedProblems); - } - - static Stream validateExistingTissuesArgs() { - Tissue tissue = EntityFactory.getTissue(); - Tissue tissue2 = EntityFactory.makeTissue(tissue.getDonor(), tissue.getSpatialLocation()); - return Stream.of( - Arguments.of(List.of(ValidateExistingTissueTestData.externalName("X1").existing(false), - ValidateExistingTissueTestData.externalName(tissue.getExternalName())), tissue, null), - Arguments.of(ValidateExistingTissueTestData.externalName("X1").existing(false), null, null), - - Arguments.of(List.of(ValidateExistingTissueTestData.externalName(null), - ValidateExistingTissueTestData.externalName(tissue.getExternalName())), - tissue, "Missing external identifier."), - Arguments.of(List.of(ValidateExistingTissueTestData.externalName("Bananas"), - ValidateExistingTissueTestData.externalName("Golf")), - null, "Existing external identifiers not recognised: [\"Bananas\", \"Golf\"]"), - Arguments.of(List.of(ValidateExistingTissueTestData.externalName(tissue.getExternalName()).fieldProblem("Bad tissue type."), - ValidateExistingTissueTestData.externalName(tissue2.getExternalName()).fieldProblem("Bad spatial location.")), - List.of(tissue, tissue2), List.of("Bad tissue type.", "Bad spatial location.")) - ); - } - - @ParameterizedTest - @MethodSource("validateCollectionDatesArgs") - public void testValidateCollectionDates(List brrs, List expectedProblems) { - RegisterRequest request = new RegisterRequest(brrs); - RegisterValidationImp validation = create(request); - validation.validateCollectionDates(); - assertThat(validation.getProblems()).containsExactlyInAnyOrderElementsOf(expectedProblems); - } - - static Stream validateCollectionDatesArgs() { - LocalDate future1 = LocalDate.now().plusDays(7); - LocalDate future2 = future1.plusDays(2); - return Arrays.stream(new Object[][]{ - { toBrr(Species.HUMAN_NAME, LifeStage.fetal, LocalDate.of(2022,1,20)) }, - { toBrr("Hamster", LifeStage.fetal, null) }, - { toBrr(Species.HUMAN_NAME, LifeStage.adult, null) }, - { toBrr(Species.HUMAN_NAME, LifeStage.paediatric, null) }, - - { toBrr(Species.HUMAN_NAME, LifeStage.fetal, LocalDate.of(2021,5,6)), - toBrr(Species.HUMAN_NAME, LifeStage.paediatric, null), - toBrr("Hamster", LifeStage.fetal, null), - toBrr(null, LifeStage.fetal, null), - toBrr(Species.HUMAN_NAME, null, null), - }, - - { toBrr(Species.HUMAN_NAME, LifeStage.fetal, null), - "Human fetal samples must have a collection date." }, - { toBrr(Species.HUMAN_NAME, LifeStage.fetal, future1), - toBrr("Hamster", LifeStage.adult, future2), - "Invalid sample collection dates: ["+future1+", "+future2+"]"}, - { toBrr(Species.HUMAN_NAME, LifeStage.fetal, LocalDate.of(2021,1,2), "Ext1"), - toBrr(Species.HUMAN_NAME, LifeStage.fetal, LocalDate.of(2021,1,3), "ext1"), - "Inconsistent collection dates specified for tissue EXT1."}, - - { toBrr(Species.HUMAN_NAME, LifeStage.fetal, LocalDate.of(2020,2,1), "EXT2"), - toBrr(Species.HUMAN_NAME, LifeStage.adult, LocalDate.of(2020,2,2), "ext1"), - toBrr(Species.HUMAN_NAME, LifeStage.adult, null, "ext1"), - toBrr(null, null, null), - toBrr(Species.HUMAN_NAME, LifeStage.fetal, null), - toBrr(Species.HUMAN_NAME, LifeStage.adult, future1), - "Human fetal samples must have a collection date.", - "Invalid sample collection date: ["+future1+"]", - "Inconsistent collection dates specified for tissue EXT1."}, - }).map(arr -> Arguments.of(Arrays.stream(arr).filter(obj -> obj instanceof BlockRegisterRequest) - .collect(toList()), - Arrays.stream(arr).filter(obj -> obj instanceof String) - .collect(toList()))); - } - - @ParameterizedTest - @CsvSource({"false,false", "true,false", "true,true"}) - public void testValidateWorks(boolean anyWorks, boolean anyProblem) { - List brrs = List.of( - toBrr(Species.HUMAN_NAME, LifeStage.adult, null) - ); - List workNumbers; - List works; - if (!anyWorks) { - workNumbers = List.of(); - works = List.of(); - } else { - workNumbers = List.of("SGP1", "SGP2"); - works = IntStream.rangeClosed(1,2).mapToObj(i -> { - Work w = new Work(); - w.setId(i); - w.setWorkNumber("SGP"+i); - return w; - }).collect(toList()); - } - - RegisterRequest request = new RegisterRequest(brrs, workNumbers); - var validation = create(request); - if (anyProblem) { - when(mockWorkService.validateUsableWorks(any(), any())).then( - Matchers.addProblem("Bad work", UCMap.from(works, Work::getWorkNumber)) - ); - } else if (anyWorks) { - when(mockWorkService.validateUsableWorks(any(), any())).thenReturn(UCMap.from(works, Work::getWorkNumber)); - } - validation.validateWorks(); - if (!anyWorks) { - verifyNoInteractions(mockWorkService); - assertProblem(validation.getProblems(), "No work number supplied."); - } else { - if (anyProblem) { - assertThat(validation.getProblems()).containsExactly("Bad work"); - } else { - assertThat(validation.getProblems()).isEmpty(); - } - verify(mockWorkService).validateUsableWorks(any(), eq(workNumbers)); - } - assertThat(validation.getWorks()).containsExactlyInAnyOrderElementsOf(works); - - } - - static BlockRegisterRequest toBrr(String species, LifeStage lifeStage, LocalDate collectionDate, String ext) { - BlockRegisterRequest brr = new BlockRegisterRequest(); - brr.setSpecies(species); - brr.setLifeStage(lifeStage); - brr.setSampleCollectionDate(collectionDate); - brr.setExternalIdentifier(ext); - return brr; - } - - static BlockRegisterRequest toBrr(String species, LifeStage lifeStage, LocalDate collectionDate) { - return toBrr(species, lifeStage, collectionDate, null); - } - - private void testValidateSimpleField(List givenStrings, - List expectedItems, List expectedProblems, - Consumer validationFunction, - Function stringFn, - BiConsumer blockFunction, - Function> mapFunction, - BiFunction getter) { - RegisterRequest request = new RegisterRequest( - givenStrings.stream() - .map(string -> { - BlockRegisterRequest br = new BlockRegisterRequest(); - blockFunction.accept(br, string); - return br; - }) - .collect(toList())); - - RegisterValidationImp validation = create(request); - validationFunction.accept(validation); - assertThat(validation.getProblems()).hasSameElementsAs(expectedProblems); - Map expectedItemMap = expectedItems.stream().collect(toMap(item -> stringFn.apply(item).toUpperCase(), h -> h)); - Map actualMap = mapFunction.apply(validation).entrySet().stream() - .filter(e -> e.getValue()!=null) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); - assertEquals(expectedItemMap, actualMap); - for (E item : expectedItems) { - assertEquals(item, getter.apply(validation, stringFn.apply(item))); - } - } - - @SuppressWarnings("unchecked") - @Test - void testValidateBioRisks() { - BlockRegisterRequest block1 = new BlockRegisterRequest(); - block1.setBioRiskCode("risk1"); - BlockRegisterRequest block2 = new BlockRegisterRequest(); - block2.setBioRiskCode("risk2"); - RegisterRequest request = new RegisterRequest(List.of(block1, block2)); - RegisterValidationImp val = create(request); - UCMap returnedMap = UCMap.from(BioRisk::getCode, new BioRisk(1, "risk1")); - when(mockBioRiskService.loadAndValidateBioRisks(any(), any(), any(), any())).thenReturn(returnedMap); - - val.validateBioRisks(); - - assertSame(returnedMap, val.bioRiskMap); - ArgumentCaptor> blockStreamCaptor = ArgumentCaptor.forClass(Stream.class); - ArgumentCaptor> getterCaptor = ArgumentCaptor.forClass(Function.class); - ArgumentCaptor> setterCaptor = ArgumentCaptor.forClass(BiConsumer.class); - verify(mockBioRiskService).loadAndValidateBioRisks(same(val.problems), blockStreamCaptor.capture(), - getterCaptor.capture(), setterCaptor.capture()); - - // Check that the getter and setter are the functions we expect - assertThat(blockStreamCaptor.getValue().map(getterCaptor.getValue())).containsExactly("risk1", "risk2"); - BiConsumer setter = setterCaptor.getValue(); - BlockRegisterRequest blk = new BlockRegisterRequest(); - setter.accept(blk, "v1"); - assertEquals("v1", blk.getBioRiskCode()); - } - - @Test - void testValidateCellClasses() { - String[] ccNames = {"cc1", "cc2", null, "cc4"}; - List blocks = IntStream.range(0, ccNames.length).mapToObj(i -> new BlockRegisterRequest()).toList(); - Zip.of(Arrays.stream(ccNames), blocks.stream()).forEach((name, block) -> block.setCellClass(name)); - CellClass[] cellClasses = IntStream.rangeClosed(1, 2).mapToObj(i -> new CellClass(i, "cc"+i, false, true)).toArray(CellClass[]::new); - UCMap ccMap = UCMap.from(CellClass::getName, cellClasses); - when(mockCellClassRepo.findMapByNameIn(any())).thenReturn(ccMap); - RegisterRequest request = new RegisterRequest(blocks); - RegisterValidationImp val = create(request); - val.validateCellClasses(); - assertThat(val.problems).containsExactlyInAnyOrder("Missing cell class name.", "Unknown cell class name: [\"cc4\"]"); - verify(mockCellClassRepo).findMapByNameIn(Set.of("cc1", "cc2", "cc4")); - assertSame(ccMap, val.cellClassMap); - } - - /** @see #testValidateDonors */ - private static Stream donorData() { - Species human = new Species(1, Species.HUMAN_NAME); - Species hamster = new Species(2, "Hamster"); - Species dodo = new Species(3, "Dodo"); - dodo.setEnabled(false); - List knownSpecies = List.of(human, hamster, dodo); - Donor dirk = new Donor(1, "Dirk", LifeStage.adult, human); - Donor jeff = new Donor(2, "Jeff", LifeStage.fetal, hamster); - Donor dodonor = new Donor(3, "Dodonor", LifeStage.adult, dodo); - // List donorNames, List lifeStages, List speciesNames, - // List knownDonors, List knownSpecies, List expectedDonors, - // List expectedProblems - return Stream.of( - // Valid: - Arguments.of(List.of("DONOR1", "Donor2"), List.of(LifeStage.adult, LifeStage.fetal), - List.of(Species.HUMAN_NAME, "hamster"), - List.of(), knownSpecies, - List.of(new Donor(null, "DONOR1", LifeStage.adult, human), - new Donor(null, "Donor2", LifeStage.fetal, hamster)), - List.of()), - Arguments.of(List.of("Donor1", "DONOR1"), List.of(LifeStage.adult, LifeStage.adult), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(), knownSpecies, - List.of(new Donor(null, "Donor1", LifeStage.adult, human)), - List.of()), - Arguments.of(List.of("DIRK", "jeff"), - List.of(dirk.getLifeStage(), jeff.getLifeStage()), List.of(Species.HUMAN_NAME, "hamster"), - List.of(dirk, jeff), knownSpecies, List.of(dirk, jeff), - List.of()), - - // Invalid: - Arguments.of(List.of("Dirk", "jeff"), List.of(LifeStage.adult, LifeStage.paediatric), - List.of(Species.HUMAN_NAME, "hamster"), - List.of(dirk, jeff), knownSpecies, List.of(dirk, jeff), - List.of("Wrong life stage given for existing donor Jeff")), - Arguments.of(List.of("Donor1", "DONOR1"), List.of(LifeStage.adult, LifeStage.fetal), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(), knownSpecies, List.of(new Donor(null, "Donor1", LifeStage.adult, human)), - List.of("Multiple different life stages specified for donor Donor1")), - Arguments.of(Arrays.asList(null, null), Arrays.asList(null, null), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(), knownSpecies, List.of(), List.of("Missing donor identifier.")), - Arguments.of(List.of(""), List.of(LifeStage.adult), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(), knownSpecies, List.of(), List.of("Missing donor identifier.")), - Arguments.of(List.of("Donor1"), List.of(LifeStage.adult), - List.of(""), List.of(), knownSpecies, List.of(new Donor(null, "Donor1", LifeStage.adult, null)), - List.of("Missing species.")), - Arguments.of(List.of("Donor1"), List.of(LifeStage.adult), List.of("Bananas"), - List.of(), knownSpecies, List.of(new Donor(null, "Donor1", LifeStage.adult, null)), - List.of("Unknown species: \"Bananas\"")), - Arguments.of(List.of("Donor1", "DONOR1"), List.of(LifeStage.adult, LifeStage.adult), - List.of(Species.HUMAN_NAME, "hamster"), - List.of(), knownSpecies, List.of(new Donor(null, "Donor1", LifeStage.adult, human)), - List.of("Multiple different species specified for donor Donor1")), - Arguments.of(List.of("Donor1", "Jeff"), List.of(LifeStage.adult, LifeStage.fetal), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(jeff), knownSpecies, List.of(jeff, new Donor(null, "Donor1", LifeStage.adult, human)), - List.of("Wrong species given for existing donor Jeff")), - Arguments.of(List.of("dodonor"), - List.of(dodonor.getLifeStage()), List.of("dodo"), - List.of(dodonor), knownSpecies, List.of(dodonor), - List.of("Species is not enabled: Dodo")), - Arguments.of(List.of("Donor1"), List.of(LifeStage.adult), List.of("dodo"), List.of(), knownSpecies, - List.of(new Donor(null, "Donor1", LifeStage.adult, dodo)), - List.of("Species is not enabled: Dodo")), - - Arguments.of(List.of("Donor1", "DONOR1", "jeff", "dirk", "", ""), - List.of(LifeStage.adult, LifeStage.fetal, LifeStage.paediatric, LifeStage.paediatric, LifeStage.adult, LifeStage.adult), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME, "hamster", Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(dirk, jeff),knownSpecies, List.of(new Donor(null, "Donor1", LifeStage.adult, human), dirk, jeff), - List.of("Multiple different life stages specified for donor Donor1", - "Wrong life stage given for existing donor Dirk", - "Wrong life stage given for existing donor Jeff", - "Missing donor identifier.")) - - ); - } - - private static Stream slData() { - final TissueType tt = new TissueType(50, "Arm", "ARM"); - final String name = tt.getName(); - final SpatialLocation sl0 = new SpatialLocation(500, "Alpha", 0, tt); - final SpatialLocation sl1 = new SpatialLocation(501, "Beta", 1, tt); - final SpatialLocation sl2 = new SpatialLocation(502, "Gamma", 2, tt); - sl2.setEnabled(false); - tt.setSpatialLocations(List.of(sl0, sl1, sl2)); - final TissueType tt2 = new TissueType(51, "Leg", "LEG"); - tt2.setEnabled(false); - final SpatialLocation sl0a = new SpatialLocation(601, "Alabama", 0, tt2); - tt2.setSpatialLocations(List.of(sl0a)); - return Stream.of( - // Valid: - Arguments.of(List.of(name, name.toUpperCase(), name.toLowerCase()), List.of(0, 0, 1), - List.of(tt), List.of(sl0, sl1), List.of()), - - // Invalid: - Arguments.of(List.of(name, "Plumbus", "Slime", "Slime"), List.of(1, 2, 3, 4), - List.of(tt), List.of(sl1), List.of("Unknown tissue types: [Plumbus, Slime]")), - Arguments.of(List.of(name, name, name), List.of(0,1,2), List.of(tt), List.of(sl0, sl1, sl2), - List.of("Spatial location is disabled: 2 for tissue type Arm.")), - Arguments.of(List.of(tt2.getName()), List.of(0), List.of(tt2), List.of(sl0a), - List.of("Tissue type \""+tt2.getName()+"\" is disabled.")), - Arguments.of(List.of(name, name, name, name, name, name), List.of(0, 0, 1, 3, 3, 4), - List.of(tt), List.of(sl0, sl1), List.of("Unknown spatial location 3 for tissue type Arm.", - "Unknown spatial location 4 for tissue type Arm.")), - Arguments.of(Arrays.asList(null, null), List.of(1,2), - List.of(tt), List.of(), List.of("Missing tissue type.")), - Arguments.of(List.of(name, "", "Plumbus", name), List.of(0, 0, 0, 5), - List.of(tt), List.of(sl0), - List.of("Missing tissue type.", "Unknown tissue type: [Plumbus]", - "Unknown spatial location 5 for tissue type Arm.")) - ); - } - - /** @see #testValidateHmdmcs */ - private static Stream hmdmcData() { - Hmdmc h0 = new Hmdmc(20000, "20/000"); - Hmdmc h1 = new Hmdmc(20001, "20/001"); - Hmdmc h2 = new Hmdmc(20002, "20/002"); - Hmdmc h3 = new Hmdmc(20003, "20/003"); - h2.setEnabled(false); - h3.setEnabled(false); - // List knownHmdmcs, List givenHmdmcs, List speciesNames, - // List expectedHmdmcs, List expectedProblems - return Stream.of( - Arguments.of(List.of(h0, h1), List.of("20/001", "20/000", "20/000", ""), List.of(Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME, "Hamster"), - List.of(h0, h1), List.of()), - Arguments.of(List.of(h0, h1), List.of("20/001", "20/404", "20/405"), List.of(Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(h1), List.of("Unknown HuMFre numbers: [20/404, 20/405]")), - Arguments.of(List.of(h0), Arrays.asList(null, "20/000", null), List.of(Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(h0), List.of("Missing HuMFre number.")), - Arguments.of(List.of(h0, h1), List.of("20/000", "20/001"), List.of(Species.HUMAN_NAME, "Hamster"), - List.of(h0), List.of("Non-human tissue should not have a HuMFre number.")), - Arguments.of(List.of(h0, h2, h3), List.of("20/000", "20/002", "20/003", "20/002"), List.of(Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(h0, h2, h3), List.of("HuMFre numbers not enabled: [20/002, 20/003]")), - Arguments.of(List.of(h0, h1, h2), List.of("20/000", "20/001", "20/000", "", "", "20/404", "20/002"), - List.of(Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME, Species.HUMAN_NAME), - List.of(h0, h1, h2), List.of("Missing HuMFre number.", "Unknown HuMFre number: [20/404]", - "HuMFre number not enabled: [20/002]")) - ); - } - - private static Stream ltData() { - LabwareType lt0 = EntityFactory.getTubeType(); - LabwareType lt1 = EntityFactory.makeLabwareType(2, 3); - String name0 = lt0.getName(); - String name1 = lt1.getName(); - return Stream.of( - Arguments.of(List.of(lt0, lt1), List.of(name1, name0, name0), List.of(lt0, lt1), List.of()), - Arguments.of(List.of(lt0, lt1), List.of(name1, "Custard", "Banana"), List.of(lt1), List.of("Unknown labware types: [Custard, Banana]")), - Arguments.of(List.of(lt0), Arrays.asList(null, name0, null), List.of(lt0), List.of("Missing labware type.")), - Arguments.of(List.of(lt0, lt1), List.of(name0, name1, name0, "", "", "Banana"), List.of(lt0, lt1), List.of("Missing labware type.", "Unknown labware type: [Banana]")) - ); - } - - private static Stream mediumData() { - Medium med = EntityFactory.getMedium(); - return Stream.of(Arguments.of(List.of(med), List.of(med.getName()), List.of(med), List.of()), - Arguments.of(List.of(), Arrays.asList(null, ""), List.of(), List.of("Missing medium.")), - Arguments.of(List.of(med), List.of(med.getName(), "Sausage", "Sausage"), List.of(med), List.of("Unknown medium: [Sausage]"))); - } - - - private static Stream fixativeData() { - Fixative fix = EntityFactory.getFixative(); - return Stream.of(Arguments.of(List.of(fix), List.of(fix.getName()), List.of(fix), List.of()), - Arguments.of(List.of(), Arrays.asList(null, ""), List.of(), List.of("Missing fixative.")), - Arguments.of(List.of(fix), List.of(fix.getName(), "Sausage", "Sausage"), List.of(fix), List.of("Unknown fixative: [Sausage]"))); - } - - - private static Stream newTissueData() { - return Stream.of( - // No problems - Arguments.of(List.of(ValidateTissueTestData.externalName("X1").replicate("1"), - ValidateTissueTestData.externalName("X2").replicate("2a")), - List.of()), - Arguments.of(List.of(ValidateTissueTestData.externalName("X1").replicate("1").anyWithSameIdentifier(true).existing(true), - ValidateTissueTestData.externalName("X2").replicate("2a")), - List.of()), - - // Some problems - Arguments.of(List.of(ValidateTissueTestData.externalName("X1").replicate("1"), - ValidateTissueTestData.externalName("X2").replicate("-4")), - List.of("Invalid replicate: -4")), - Arguments.of(List.of(ValidateTissueTestData.externalName("X1").replicate("1"), - ValidateTissueTestData.externalName("X2").replicate("2").highestSection(-2)), - List.of("Highest section number cannot be negative.")), - Arguments.of(List.of(ValidateTissueTestData.externalName(null)), - List.of("Missing external identifier.")), - Arguments.of(List.of(ValidateTissueTestData.externalName("")), - List.of("Missing external identifier.")), - Arguments.of(List.of(ValidateTissueTestData.externalName("Banana*")), - List.of("Invalid name: Banana*")), - Arguments.of(List.of(ValidateTissueTestData.externalName("xyz").replicate("1"), - ValidateTissueTestData.externalName("Xyz").replicate("2")), - List.of("Repeated external identifier: Xyz")), - Arguments.of(List.of(ValidateTissueTestData.externalName("X1").replicate("1"), - ValidateTissueTestData.externalName("X2").replicate("2").anyWithSameIdentifier(true)), - List.of("There is already tissue in the database with external identifier X2.")), - Arguments.of(List.of(ValidateTissueTestData.externalName("X1").replicate("1").anyWithSameIdentifier(true), - ValidateTissueTestData.externalName("X1").replicate("2").anyWithSameIdentifier(true), - ValidateTissueTestData.externalName("X2").replicate("3").anyWithSameIdentifier(true)), - List.of("There is already tissue in the database with external identifier X1.", - "There is already tissue in the database with external identifier X2.", - "Repeated external identifier: X1")), - - // Many problems - Arguments.of(List.of(ValidateTissueTestData.externalName("X1*").replicate("-1").highestSection(-1), - ValidateTissueTestData.externalName(null).replicate("2"), - ValidateTissueTestData.externalName("X1").replicate("3").anySimilarInDatabase(true), - ValidateTissueTestData.externalName("X2").replicate("4").anyWithSameIdentifier(true), - ValidateTissueTestData.externalName("X3").replicate("5"), - ValidateTissueTestData.externalName("X4").replicate("5"), - ValidateTissueTestData.externalName("X4").replicate("6")), - List.of("Invalid replicate: -1", - "Highest section number cannot be negative.", - "Missing external identifier.", - "Invalid name: X1*", - "Repeated external identifier: X4", - "There is already tissue in the database with external identifier X2.")) - ); - } - - static class ValidateTissueTestData { - String externalName; - String replicate = "1"; - String donorName = "D"; - String tissueTypeName = "TT"; - int slCode = 2; - String mediumName = "M"; - String fixativeName = "F"; - int highestSection = 0; - boolean anySimilarInDatabase; - boolean anyWithSameIdentifier; - boolean existing; - - public ValidateTissueTestData(String externalName) { - this.externalName = externalName; - } - - public static ValidateTissueTestData externalName(String externalName) { - return new ValidateTissueTestData(externalName); - } - - public ValidateTissueTestData replicate(String replicate) { - this.replicate = replicate; - return this; - } - - public ValidateTissueTestData highestSection(int highestSection) { - this.highestSection = highestSection; - return this; - } - - public ValidateTissueTestData anySimilarInDatabase(boolean anySimilarInDatabase) { - this.anySimilarInDatabase = anySimilarInDatabase; - return this; - } - - public ValidateTissueTestData anyWithSameIdentifier(boolean anyWithSameIdentifier) { - this.anyWithSameIdentifier = anyWithSameIdentifier; - return this; - } - - public ValidateTissueTestData existing(boolean existing) { - this.existing = existing; - return this; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("externalName", externalName) - .add("replicate", replicate) - .add("donorName", donorName) - .add("tissueTypeName", tissueTypeName) - .add("slCode", slCode) - .add("mediumName", mediumName) - .add("highestSection", highestSection) - .add("fixativeName", fixativeName) - .add("anySimilarInDatabase", anySimilarInDatabase) - .add("anyWithSameIdentifier", anyWithSameIdentifier) - .add("existing", existing) - .toString(); - } - } - - private static class ValidateExistingTissueTestData { - String externalName; - boolean existing = true; - String fieldProblem = null; - - static ValidateExistingTissueTestData externalName(String externalName) { - ValidateExistingTissueTestData td = new ValidateExistingTissueTestData(); - td.externalName = externalName; - return td; - } - - ValidateExistingTissueTestData existing(boolean existing) { - this.existing = existing; - return this; - } - - ValidateExistingTissueTestData fieldProblem(String fieldProblem) { - this.fieldProblem = fieldProblem; - return this; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("externalName", externalName) - .add("existing", existing) - .add("fieldProblem", fieldProblem) - .omitNullValues() - .toString(); - } - } -} diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java index caa638e02..7c5afa3d8 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java @@ -1,42 +1,36 @@ package uk.ac.sanger.sccp.stan.service.register; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import org.junit.jupiter.api.*; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; import uk.ac.sanger.sccp.stan.request.register.SectionRegisterRequest; -import uk.ac.sanger.sccp.stan.service.*; -import uk.ac.sanger.sccp.stan.service.sanitiser.Sanitiser; -import uk.ac.sanger.sccp.stan.service.work.WorkService; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static uk.ac.sanger.sccp.stan.Matchers.genericMock; /** * Tests {@link RegisterValidationFactory} * @author dr6 */ public class TestRegisterValidationFactory { + @InjectMocks RegisterValidationFactory registerValidationFactory; + private AutoCloseable mocking; + @BeforeEach void setup() { - Validator mockStringValidator = genericMock(Validator.class); - Sanitiser mockSanitiser = genericMock(Sanitiser.class); - registerValidationFactory = new RegisterValidationFactory( - mock(DonorRepo.class), mock(HmdmcRepo.class), mock(TissueTypeRepo.class), - mock(LabwareTypeRepo.class), mock(MediumRepo.class), - mock(FixativeRepo.class), mock(TissueRepo.class), mock(SpeciesRepo.class), mock(LabwareRepo.class), - mock(BioStateRepo.class), mock(CellClassRepo.class), mockStringValidator, mockStringValidator, mockStringValidator, - mockStringValidator, mockStringValidator, mockSanitiser, mockStringValidator, mockStringValidator, mockStringValidator, - mock(TissueFieldChecker.class), mock(SlotRegionService.class), mock(BioRiskService.class), - mock(WorkService.class)); + mocking = MockitoAnnotations.openMocks(this); + } + + @AfterEach + void cleanup() throws Exception { + mocking.close(); } @Test - public void testCreateRegisterValidation() { - assertNotNull(registerValidationFactory.createRegisterValidation(new RegisterRequest())); + public void testCreateBlockRegisterValidation() { + assertNotNull(registerValidationFactory.createBlockRegisterValidation(new BlockRegisterRequest())); } @Test diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestTissueFieldChecker.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestTissueFieldChecker.java deleted file mode 100644 index aaa15c19d..000000000 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestTissueFieldChecker.java +++ /dev/null @@ -1,122 +0,0 @@ -package uk.ac.sanger.sccp.stan.service.register; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.ac.sanger.sccp.stan.EntityFactory; -import uk.ac.sanger.sccp.stan.model.Tissue; -import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; - -import java.time.LocalDate; -import java.util.*; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Tests {@link TissueFieldChecker} - * @author dr6 - */ -public class TestTissueFieldChecker { - TissueFieldChecker checker; - - @BeforeEach - void setup() { - checker = new TissueFieldChecker(); - } - - @ParameterizedTest - @MethodSource("matchArgs") - public void testMatch(Object a, Object b, boolean expected) { - assertEquals(expected, checker.match(a, b)); - } - - static Stream matchArgs() { - return Stream.of( - Arguments.of("Alpha", "alpha", true), - Arguments.of(null, null, true), - Arguments.of(null, "", true), - Arguments.of(11, 11, true), - - Arguments.of("Alpha", "beta", false), - Arguments.of("Alpha", null, false), - Arguments.of("Alpha", 11, false), - Arguments.of(null, "hi", false), - Arguments.of(null, 11, false), - Arguments.of(11, null, false), - Arguments.of(11, "bananas", false) - ); - } - - @ParameterizedTest - @MethodSource("checkArgs") - public void testCheck(BlockRegisterRequest br, Tissue tissue, Object problemObj) { - Collection expectedProblems; - if (problemObj==null) { - expectedProblems = List.of(); - } else if (problemObj instanceof Collection) { - //noinspection unchecked - expectedProblems = (Collection) problemObj; - } else { - expectedProblems = List.of((String) problemObj); - } - - List problems = new ArrayList<>(); - checker.check(problems::add, br, tissue); - - assertThat(problems).containsExactlyInAnyOrderElementsOf(expectedProblems); - } - - static Stream checkArgs() { - final Tissue tissue = EntityFactory.getTissue(); - final String forTissue = " for existing tissue "+tissue.getExternalName()+"."; - final Tissue tissueWithDate = EntityFactory.makeTissue(tissue.getDonor(), tissue.getSpatialLocation()); - tissueWithDate.setCollectionDate(LocalDate.of(2021,2,3)); - - return Stream.of( - Arguments.of(toBRR(tissue, null), tissue, null), - Arguments.of(toBRR(tissueWithDate, null), tissueWithDate, null), - Arguments.of(toBRR(tissue, br -> br.setSampleCollectionDate(LocalDate.of(2020,1,2))), tissue, null), - Arguments.of(toBRR(tissue, br -> br.setDonorIdentifier("Foo")), tissue, "Expected donor identifier to be "+tissue.getDonor().getDonorName()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setHmdmc("12/345")), tissue, "Expected HuMFre number to be "+tissue.getHmdmc().getHmdmc()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setTissueType("Plumbus")), tissue, "Expected tissue type to be "+tissue.getTissueType().getName()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setSpatialLocation(18)), tissue, "Expected spatial location to be "+tissue.getSpatialLocation().getCode()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setReplicateNumber("-5")), tissue, "Expected replicate number to be "+tissue.getReplicate()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setMedium("Custard")), tissue, "Expected medium to be "+tissue.getMedium().getName()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setFixative("Glue")), tissue, "Expected fixative to be "+tissue.getFixative().getName()+forTissue), - Arguments.of(toBRR(tissue, br -> br.setCellClass("cc4")), tissue, "Expected cellular classification to be Tissue"+forTissue), - Arguments.of(toBRR(tissueWithDate, br -> br.setSampleCollectionDate(LocalDate.of(2020,1,2))), tissueWithDate, "Expected sample collection date to be "+tissueWithDate.getCollectionDate()+" for existing tissue "+tissueWithDate.getExternalName()+"."), - Arguments.of(toBRR(tissue, br -> { - br.setDonorIdentifier("Foo"); - br.setHmdmc("12/345"); - br.setTissueType("Plumbus"); - }), tissue, - List.of("Expected donor identifier to be "+tissue.getDonor().getDonorName()+forTissue, - "Expected HuMFre number to be "+tissue.getHmdmc().getHmdmc()+forTissue, - "Expected tissue type to be "+tissue.getTissueType().getName()+forTissue) - ) - ); - } - - private static BlockRegisterRequest toBRR(Tissue tissue, Consumer adjuster) { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExistingTissue(true); - br.setExternalIdentifier(tissue.getExternalName()); - br.setTissueType(tissue.getTissueType().getName()); - br.setDonorIdentifier(tissue.getDonor().getDonorName()); - br.setHmdmc(tissue.getHmdmc().getHmdmc()); - br.setSpatialLocation(tissue.getSpatialLocation().getCode()); - br.setReplicateNumber(tissue.getReplicate()); - br.setMedium(tissue.getMedium().getName()); - br.setFixative(tissue.getFixative().getName()); - br.setSampleCollectionDate(tissue.getCollectionDate()); - br.setCellClass(tissue.getCellClass().getName()); - if (adjuster!=null) { - adjuster.accept(br); - } - return br; - } -} diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java index 6fa9854d6..3ca8a0500 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java @@ -5,20 +5,18 @@ import org.apache.poi.ss.util.CellAddress; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.*; import uk.ac.sanger.sccp.stan.Matchers; +import uk.ac.sanger.sccp.stan.model.Address; import uk.ac.sanger.sccp.stan.model.LifeStage; -import uk.ac.sanger.sccp.stan.model.Species; -import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; -import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.request.register.*; import uk.ac.sanger.sccp.stan.service.register.filereader.BlockRegisterFileReader.Column; -import uk.ac.sanger.sccp.utils.Zip; import java.io.IOException; import java.time.LocalDate; import java.util.*; +import java.util.function.Supplier; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -26,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import static uk.ac.sanger.sccp.stan.Matchers.*; /** * Tests {@link BlockRegisterFileReaderImp} @@ -58,7 +57,7 @@ void testRead_headingProblems() { mockSheet(); mockHeadingRow(); doAnswer(Matchers.addProblem(problem, map)).when(reader).indexColumns(any(), any()); - assertValidationError(() -> reader.read(sheet), problem); + assertValidationException(() -> reader.read(sheet), List.of(problem)); verify(reader).indexColumns(any(), same(headingRow)); verify(reader, never()).readRow(any(), any(), any()); verify(reader, never()).createRequest(any(), any()); @@ -71,7 +70,7 @@ void testRead_noData() { mockRows(0,0); Map map = columnMapOf(Column.Donor_identifier, 3); doReturn(map).when(reader).indexColumns(any(), any()); - assertValidationError(() -> reader.read(sheet), "No registrations requested."); + assertValidationException(() -> reader.read(sheet), List.of("No registrations requested.")); verify(reader, never()).readRow(any(), any(), any()); verify(reader, never()).createRequest(any(), any()); } @@ -93,15 +92,15 @@ void testRead(boolean error) { Matchers.mayAddProblem(rowProblem, rowMaps.get(i)).when(reader).readRow(any(), any(), same(rows.get(DATA_ROW+i))); } - RegisterRequest request; + BlockRegisterRequest request; if (error) { request = null; } else { - request = new RegisterRequest(); + request = new BlockRegisterRequest(); doReturn(request).when(reader).createRequest(any(), any()); } if (error) { - assertValidationError(() -> reader.read(sheet), problem); + assertValidationException(() -> reader.read(sheet), List.of(problem)); } else { assertSame(request, reader.read(sheet)); } @@ -121,8 +120,10 @@ void testRead(boolean error) { void testIndexColumns() { Row row = mockRow("All information is needed", "SGP number", "donor identifier", "life stage", "if then date of collection of stuff", - "species", "cellular classification", "biological risk assessment number", "humfre", "tissue type", "external id", "spatial location", "replicate number", - "last known banana section custard", "labware type", "fixative", "medium", "information", "comment"); + "species", "cellular classification", "biological risk assessment number", "humfre", "slot address of sample", + "tissue type", "external id", "spatial location", "replicate number", + "last known banana section custard", "labware type", "fixative", "medium", "external barcode", + "information", "comment"); List problems = new ArrayList<>(); var result = reader.indexColumns(problems, row); assertThat(problems).isEmpty(); @@ -146,7 +147,7 @@ void testIndexColumnsProblems() { assertThat(problems).containsExactlyInAnyOrder( "Repeated column: Work number", "Unexpected column heading: \"bananas\"", - "Missing columns: [Tissue type, External identifier]"); + "Missing columns: [Slot address, Tissue type, External identifier, External barcode]"); } @Test @@ -301,108 +302,161 @@ static Stream cellValueMocks() { }).map(Arguments::of); } - @Test - void testCreateRequest() { - List> rows = List.of( - rowMap("SGP1, SGP2 sgp3,sgp2", "X1"), - rowMap("sgp1 sgp3 sgp2", "X2"), - rowMap(null, null) - ); - List brs = IntStream.rangeClosed(1, rows.size()) - .mapToObj(i -> makeBlockRegisterRequest("X"+i)) - .collect(toList()); - Zip.of(rows.stream(), brs.stream()).forEach((row, br) -> doReturn(br).when(reader).createBlockRequest(any(), same(row))); - - final List problems = new ArrayList<>(); - RegisterRequest request = reader.createRequest(problems, rows); - assertThat(request.getWorkNumbers()).containsExactlyInAnyOrder("SGP1", "SGP2", "SGP3"); - assertEquals(brs, request.getBlocks()); + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testCreateRequest(boolean ok) { + List> rows; + if (ok) { + rows = List.of(rowMap("SGP1, SGP2", "EXT1"), rowMap("sgp2, sgp1", "EXT2")); + } else { + rows = List.of(rowMap("SGP1", "EXT1"), rowMap("SGP2", "EXT2")); + } + List brlw = List.of(new BlockRegisterLabware()); + String problem = ok ? null : "Bad request."; + mayAddProblem(problem, brlw).when(reader).createLabwareRequests(any(), any()); + if (ok) { + List problems = new ArrayList<>(); + BlockRegisterRequest request = reader.createRequest(problems, rows); + assertSame(brlw, request.getLabware()); + assertThat(request.getWorkNumbers()).containsExactlyInAnyOrder("SGP1", "SGP2"); + } else { + List problems = new ArrayList<>(2); + List expectedProblems = List.of(problem, "All rows must list the same work numbers."); + assertValidationException(() -> reader.createRequest(problems, rows), expectedProblems); + } + verify(reader).createLabwareRequests(any(), same(rows)); } - @Test - void testCreateRequest_problems() { - List> rows = List.of( - rowMap("SGP1", "X1"), - rowMap("sgp2", "X2") - ); - List srls = IntStream.range(1, 3) - .mapToObj(i -> makeBlockRegisterRequest("X"+i)) - .toList(); - - doReturn(srls.get(0)).when(reader).createBlockRequest(any(), same(rows.get(0))); - Matchers.mayAddProblem("Bad stuff.", srls.get(1)).when(reader).createBlockRequest(any(), same(rows.get(1))); - - assertValidationError(() -> reader.createRequest(new ArrayList<>(), rows), - "All rows must list the same work numbers.", - "Bad stuff."); + @ParameterizedTest + @CsvSource(value = { + ";", + "sgp1;SGP1", + "sgp1, SGP2;SGP1,SGP2" + }, delimiter=';') + void testWorkNumberSet(String input, String expected) { + Set workNumbers = BlockRegisterFileReaderImp.workNumberSet(input); + if (expected==null) { + assertThat(workNumbers).isNullOrEmpty(); + } else { + assertThat(workNumbers).containsExactlyInAnyOrder(expected.split(",")); + } } - @Test - void testCreateBlockRegisterRequest_basic() { - List problems = new ArrayList<>(0); - Map row = new EnumMap<>(Column.class); - row.put(Column.Donor_identifier, "Donor1"); - row.put(Column.Replicate_number, "12A"); - row.put(Column.Spatial_location, 14); - row.put(Column.Last_known_section, 18); - BlockRegisterRequest src = reader.createBlockRequest(problems, row); - assertEquals("Donor1", src.getDonorIdentifier()); - assertEquals("12A", src.getReplicateNumber()); - assertEquals(14, src.getSpatialLocation()); - assertNull(src.getLifeStage()); - assertThat(problems).isEmpty(); + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testCreateLabwareRequests(boolean xbMissing) { + List> rows = List.of(rowWithExternalBarcode("EXT1", "A"), + rowWithExternalBarcode(xbMissing ? null : "ext1", "B"), + rowWithExternalBarcode("EXT2", "C")); + + List brls = List.of(new BlockRegisterLabware(), new BlockRegisterLabware()); + brls.getFirst().setExternalBarcode("EXT1"); + brls.getLast().setExternalBarcode("EXT2"); + doReturnFrom(brls.iterator()).when(reader).toLabwareRequest(any(), any()); + List problems = new ArrayList<>(xbMissing ? 1 : 0); + assertThat(reader.createLabwareRequests(problems, rows)).containsExactlyElementsOf(brls); + assertProblem(problems, xbMissing ? "Cannot process blocks without an external barcode." : null); + + verify(reader, times(2)).toLabwareRequest(any(), any()); + verify(reader).toLabwareRequest(same(problems), eq(rows.subList(0, xbMissing ? 1 : 2))); + verify(reader).toLabwareRequest(same(problems), eq(rows.subList(2, 3))); } + @ParameterizedTest + @CsvSource({ + ",false,", + "EXT1 ext1,false,EXT1", + "ext1 ext2,true,ext1", + }) + void testUniqueRowValue(String values, boolean error, String expectedValue) { + String[] splitValues = (values==null ? new String[0] : values.split("\\s+")); + final Column column = Column.External_barcode; + List> rows = Arrays.stream(splitValues).map(v -> { + Map map = new EnumMap<>(Column.class); + map.put(column, v); + return map; + }).toList(); + List problems = new ArrayList<>(error ? 1 : 0); + Supplier errorSupplier = () -> "Bad thing."; + assertEquals(expectedValue, reader.uniqueRowValue(problems, rows, column, errorSupplier)); + assertProblem(problems, error ? "Bad thing." : null); + } @Test - void testCreateBlockRegisterRequest_full() { + void testToLabwareRequest() { + List> rows = List.of(rowWithExternalBarcode("BC", "A"), rowWithExternalBarcode("BC", "B")); + rows.forEach(row -> { + row.put(Column.Labware_type, "lt"); + row.put(Column.Fixative, "fix"); + row.put(Column.Embedding_medium, "med"); + }); + BlockRegisterSample brs1 = new BlockRegisterSample(); + BlockRegisterSample brs2 = new BlockRegisterSample(); + brs1.setExternalIdentifier("xn1"); + brs2.setExternalIdentifier("xn2"); + doReturn(brs1, brs2).when(reader).toSample(any(), any()); List problems = new ArrayList<>(0); - Map row = new EnumMap<>(Column.class); - final LocalDate date = LocalDate.of(2022, 5, 5); - row.put(Column.Donor_identifier, "Donor1"); - row.put(Column.External_identifier, "X11"); - row.put(Column.HuMFre, "12/234"); - row.put(Column.Life_stage, "ADULT"); - row.put(Column.Replicate_number, "14"); - row.put(Column.Species, Species.HUMAN_NAME); - row.put(Column.Tissue_type, "Arm"); - row.put(Column.Spatial_location, 2); - row.put(Column.Embedding_medium, "brass"); - row.put(Column.Fixative, "Floop"); - row.put(Column.Collection_date, date); - row.put(Column.Last_known_section, 17); - row.put(Column.Labware_type, "Eggcup"); - BlockRegisterRequest br = reader.createBlockRequest(problems, row); - assertEquals("Donor1", br.getDonorIdentifier()); - assertEquals("X11", br.getExternalIdentifier()); - assertEquals("12/234", br.getHmdmc()); - assertEquals(LifeStage.adult, br.getLifeStage()); - assertEquals("14", br.getReplicateNumber()); - assertEquals(Species.HUMAN_NAME, br.getSpecies()); - assertEquals("Arm", br.getTissueType()); - assertEquals(2, br.getSpatialLocation()); - assertEquals("brass", br.getMedium()); - assertEquals("Floop", br.getFixative()); - assertEquals(date, br.getSampleCollectionDate()); - assertEquals(17, br.getHighestSection()); - assertEquals("Eggcup", br.getLabwareType()); + BlockRegisterLabware brl = reader.toLabwareRequest(problems, rows); + verify(reader, times(3)).uniqueRowValue(any(), any(), any(), any()); + rows.forEach(row -> verify(reader).toSample(same(problems), same(row))); assertThat(problems).isEmpty(); + assertEquals("lt", brl.getLabwareType()); + assertEquals("fix", brl.getFixative()); + assertEquals("med", brl.getMedium()); + assertEquals("BC", brl.getExternalBarcode()); + assertThat(brl.getSamples()).containsExactly(brs1, brs2); } - @Test - void testCreateRequestContent_problems() { - List problems = new ArrayList<>(2); + @ParameterizedTest + @ValueSource(booleans={false,true}) + void testToSample(boolean complete) { Map row = new EnumMap<>(Column.class); - row.put(Column.Donor_identifier, "Donor1"); - row.put(Column.Life_stage, "ascended"); - BlockRegisterRequest src = reader.createBlockRequest(problems, row); - assertThat(problems).containsExactlyInAnyOrder( - "Unknown life stage: \"ascended\"", - "Last known section not specified.", - "Spatial location not specified." - ); - assertEquals("Donor1", src.getDonorIdentifier()); - assertNull(src.getLifeStage()); + row.put(Column.Bio_risk, "risk1"); + row.put(Column.Cell_class, "cclass1"); + row.put(Column.Donor_identifier, "donor1"); + row.put(Column.HuMFre, "hum1"); + row.put(Column.Life_stage, "fetal"); + row.put(Column.Replicate_number, "17"); + row.put(Column.Species, "mermaid"); + row.put(Column.Tissue_type, "leg"); + LocalDate date; + if (complete) { + row.put(Column.Spatial_location, 3); + row.put(Column.Last_known_section, 5); + date = LocalDate.of(2023, 1, 2); + row.put(Column.Collection_date, date); + row.put(Column.Slot_address, "A1, A2"); + } else { + date = null; + } + row.put(Column.External_identifier, "ext1"); + List problems = new ArrayList<>(complete ? 0 : 2); + BlockRegisterSample brs = reader.toSample(problems, row); + if (complete) { + assertThat(problems).isEmpty(); + } else { + assertThat(problems).containsExactlyInAnyOrder("Spatial location not specified.", "Last known section not specified."); + } + assertEquals("risk1", brs.getBioRiskCode()); + assertEquals("cclass1", brs.getCellClass()); + assertEquals("donor1", brs.getDonorIdentifier()); + assertEquals("hum1", brs.getHmdmc()); + assertEquals(LifeStage.fetal, brs.getLifeStage()); + assertEquals("17", brs.getReplicateNumber()); + assertEquals("mermaid", brs.getSpecies()); + assertEquals("leg", brs.getTissueType()); + if (complete) { + assertEquals(3, brs.getSpatialLocation()); + assertEquals(5, brs.getHighestSection()); + assertEquals(date, brs.getSampleCollectionDate()); + assertThat(brs.getAddresses()).containsExactlyInAnyOrder(new Address(1,1), new Address(1,2)); + } else { + assertNull(brs.getSpatialLocation()); + assertNull(brs.getHighestSection()); + assertNull(brs.getSampleCollectionDate()); + assertThat(brs.getAddresses()).isEmpty(); + } + assertEquals("ext1", brs.getExternalIdentifier()); } static Map columnMapOf(Column k1, V v1) { @@ -418,13 +472,32 @@ static Map rowMap(Object workNumber, Object externalName) { return map; } - static BlockRegisterRequest makeBlockRegisterRequest(String externalId) { - BlockRegisterRequest br = new BlockRegisterRequest(); - br.setExternalIdentifier(externalId); - return br; + static Map rowWithExternalBarcode(String externalBarcode, String externalName) { + Map map = new EnumMap<>(Column.class); + map.put(Column.External_identifier, externalName); + map.put(Column.External_barcode, externalBarcode); + return map; + } + + @ParameterizedTest + @MethodSource("parseAddressesArgs") + void testParseAddresses(String input, List
expectedAddresses, String expectedError) { + List problems = new ArrayList<>(expectedError==null ? 0 : 1); + assertEquals(expectedAddresses, BlockRegisterFileReaderImp.parseAddresses(problems, input)); + assertProblem(problems, expectedError); } - static void assertValidationError(Executable exec, String... expectedProblems) { - Matchers.assertValidationException(exec, "The file contents are invalid.", expectedProblems); + static Stream parseAddressesArgs() { + Address A1 = new Address(1,1); + Address A2 = new Address(1,2); + Address AA50 = new Address(27,50); + return Arrays.stream(new Object[][] { + {null, List.of(), null}, + {"A1", List.of(A1), null}, + {"a1, A2", List.of(A1, A2), null}, + {"A1 A2", List.of(A1, A2), null}, + {"A2 27,50 a1", List.of(A2, AA50, A1), null}, + {"A,X,W?", List.of(), "Couldn't parse slot addresses: \"A,X,W?\""} + }).map(Arguments::of); } } \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java index febd03971..e4f75153e 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java @@ -352,26 +352,12 @@ public void testLoadSnapshots() { @Test public void testLoadLastSection() { LabwareType lt = EntityFactory.getTubeType(); - Sample sampleA = EntityFactory.getSample(); - Tissue tissueB = EntityFactory.makeTissue(EntityFactory.getDonor(), EntityFactory.getSpatialLocation()); - Sample sampleB = new Sample(60, null, tissueB, EntityFactory.getBioState()); - Sample[] samples = { sampleA, sampleB, sampleA, sampleA, sampleB, sampleA }; - boolean[] isBlock = { true, true, true, true, true, false }; - Integer[] blockMaxSection = { 6, 6, 2, null, null, null }; - - Labware[] labware = IntStream.range(0, samples.length) - .mapToObj(i -> { - Sample sample = samples[i]; - Labware lw = EntityFactory.makeLabware(lt, sample); - if (isBlock[i]) { - Slot slot = lw.getFirstSlot(); - slot.setBlockSampleId(sample.getId()); - if (blockMaxSection[i]!=null) { - slot.setBlockHighestSection(blockMaxSection[i]); - } - } - return lw; - }) + Sample[] samples = EntityFactory.makeSamples(3); + samples[0].setBlockHighestSection(6); + samples[1].setBlockHighestSection(2); + + Labware[] labware = Arrays.stream(samples) + .map(sample -> EntityFactory.makeLabware(lt, sample)) .toArray(Labware[]::new); List entries = Arrays.stream(labware) @@ -380,7 +366,7 @@ public void testLoadLastSection() { service.loadLastSection(entries); - Integer[] expectedLastSection = {6, 6, 2, null, null, null}; + Integer[] expectedLastSection = {6, 2, null}; IntStream.range(0, expectedLastSection.length).forEach(i -> assertEquals(expectedLastSection[i], entries.get(i).getLastSection(), "element "+i) ); diff --git a/src/test/resources/graphql/register.graphql b/src/test/resources/graphql/register.graphql index 06b55ee52..9b8aaf64e 100644 --- a/src/test/resources/graphql/register.graphql +++ b/src/test/resources/graphql/register.graphql @@ -1,22 +1,26 @@ mutation { - register(request:{ - blocks:[ + registerBlocks(request:{ + labware:[ { labwareType:"proviasette", - donorIdentifier:"DONOR1", - species: "Homo sapiens (Human)", - externalIdentifier:"TISSUE1", - lifeStage:adult, - hmdmc:"20/0002", - spatialLocation:0, - tissueType:"Bone", - replicateNumber:"1", medium:"None", fixative:"None", - highestSection:0, - sampleCollectionDate: "2021-02-03", - bioRiskCode: "biorisk1", - cellClass: "Tissue" + externalBarcode: "XBC1" + samples: { + donorIdentifier:"DONOR1", + species: "Homo sapiens (Human)", + externalIdentifier:"TISSUE1", + lifeStage:adult, + hmdmc:"20/0002", + spatialLocation:0, + tissueType:"Bone", + replicateNumber:"1", + highestSection:0, + sampleCollectionDate: "2021-02-03", + bioRiskCode: "biorisk1", + cellClass: "Tissue" + addresses: ["A1"] + } } ] workNumbers: ["SGP1"] diff --git a/src/test/resources/graphql/tissueblock.graphql b/src/test/resources/graphql/tissueblock.graphql index 893eb0616..fa43aa702 100644 --- a/src/test/resources/graphql/tissueblock.graphql +++ b/src/test/resources/graphql/tissueblock.graphql @@ -3,9 +3,15 @@ mutation { discardSourceBarcodes: ["BARCODE"] workNumber: "WORKNUMBER" labware: [{ - sourceBarcode: "BARCODE" labwareType: "Tube" - replicate: "5c" + contents: [ + { + sourceBarcode: "BARCODE" + replicate: "5c" + addresses: ["A1"] + sourceSampleId: 999 + } + ] }] }) { labware { diff --git a/src/test/resources/testdata/block_reg.xlsx b/src/test/resources/testdata/block_reg.xlsx index 1fef254ce..ff1c3d9cb 100644 Binary files a/src/test/resources/testdata/block_reg.xlsx and b/src/test/resources/testdata/block_reg.xlsx differ diff --git a/src/test/resources/testdata/block_reg_existing.xlsx b/src/test/resources/testdata/block_reg_existing.xlsx index e3cb4aae2..3c5d58150 100644 Binary files a/src/test/resources/testdata/block_reg_existing.xlsx and b/src/test/resources/testdata/block_reg_existing.xlsx differ diff --git a/src/test/resources/testdata/reg_empty.xlsx b/src/test/resources/testdata/reg_empty.xlsx index f6cce3c40..af3d705b6 100644 Binary files a/src/test/resources/testdata/reg_empty.xlsx and b/src/test/resources/testdata/reg_empty.xlsx differ