diff --git a/src/main/java/ca/gc/aafc/seqdb/api/ResourceRepositoryConfig.java b/src/main/java/ca/gc/aafc/seqdb/api/ResourceRepositoryConfig.java index 4a6ebaca..f080ac23 100644 --- a/src/main/java/ca/gc/aafc/seqdb/api/ResourceRepositoryConfig.java +++ b/src/main/java/ca/gc/aafc/seqdb/api/ResourceRepositoryConfig.java @@ -1,6 +1,12 @@ package ca.gc.aafc.seqdb.api; +import io.crnk.core.engine.registry.ResourceRegistry; +import javax.inject.Inject; + import ca.gc.aafc.dina.DinaBaseApiAutoConfiguration; +import ca.gc.aafc.seqdb.api.dto.SequenceManagedAttributeDto; +import ca.gc.aafc.seqdb.api.util.ManagedAttributeIdMapper; + import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -11,4 +17,14 @@ @EntityScan("ca.gc.aafc.seqdb.api.entities") @ComponentScan(basePackageClasses = DinaBaseApiAutoConfiguration.class) public class ResourceRepositoryConfig { + + @Inject + @SuppressWarnings({"deprecation", "unchecked"}) + public void setupManagedAttributeLookup(ResourceRegistry resourceRegistry) { + var resourceInfo = resourceRegistry.getEntry(SequenceManagedAttributeDto.class) + .getResourceInformation(); + + resourceInfo.setIdStringMapper( + new ManagedAttributeIdMapper(resourceInfo.getIdStringMapper())); + } } diff --git a/src/main/java/ca/gc/aafc/seqdb/api/dto/GenericMolecularAnalysisDto.java b/src/main/java/ca/gc/aafc/seqdb/api/dto/GenericMolecularAnalysisDto.java index b5580ac9..81b3c0f1 100644 --- a/src/main/java/ca/gc/aafc/seqdb/api/dto/GenericMolecularAnalysisDto.java +++ b/src/main/java/ca/gc/aafc/seqdb/api/dto/GenericMolecularAnalysisDto.java @@ -1,9 +1,12 @@ package ca.gc.aafc.seqdb.api.dto; +import io.crnk.core.resource.annotations.JsonApiField; import io.crnk.core.resource.annotations.JsonApiId; import io.crnk.core.resource.annotations.JsonApiRelation; import io.crnk.core.resource.annotations.JsonApiResource; +import io.crnk.core.resource.annotations.PatchStrategy; import java.time.OffsetDateTime; +import java.util.Map; import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Builder; @@ -41,4 +44,11 @@ public class GenericMolecularAnalysisDto { @JsonApiRelation private ExternalRelationDto protocol; + /** + * Map of Managed attribute key to value object. + */ + @JsonApiField(patchStrategy = PatchStrategy.SET) + @Builder.Default + private Map managedAttributes = Map.of(); + } diff --git a/src/main/java/ca/gc/aafc/seqdb/api/entities/GenericMolecularAnalysis.java b/src/main/java/ca/gc/aafc/seqdb/api/entities/GenericMolecularAnalysis.java index 4686a85d..fc4a933b 100644 --- a/src/main/java/ca/gc/aafc/seqdb/api/entities/GenericMolecularAnalysis.java +++ b/src/main/java/ca/gc/aafc/seqdb/api/entities/GenericMolecularAnalysis.java @@ -1,6 +1,7 @@ package ca.gc.aafc.seqdb.api.entities; import java.time.OffsetDateTime; +import java.util.Map; import java.util.UUID; import javax.persistence.Column; import javax.persistence.Entity; @@ -20,6 +21,7 @@ import org.hibernate.annotations.Generated; import org.hibernate.annotations.GenerationTime; import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; import ca.gc.aafc.dina.entity.DinaEntity; @@ -61,4 +63,10 @@ public class GenericMolecularAnalysis implements DinaEntity { @Size(max = 50) private String analysisType; + @Type(type = "jsonb") + @NotNull + @Builder.Default + @Column(name = "managed_attributes") + private Map managedAttributes = Map.of(); + } diff --git a/src/main/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisService.java b/src/main/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisService.java index c807e8a7..f4b77d8b 100644 --- a/src/main/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisService.java +++ b/src/main/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisService.java @@ -3,19 +3,33 @@ import java.util.UUID; import lombok.NonNull; +import org.apache.commons.collections.MapUtils; import org.springframework.stereotype.Service; import org.springframework.validation.SmartValidator; import ca.gc.aafc.dina.jpa.BaseDAO; import ca.gc.aafc.dina.service.DefaultDinaService; import ca.gc.aafc.seqdb.api.entities.GenericMolecularAnalysis; +import ca.gc.aafc.seqdb.api.entities.SequenceManagedAttribute; +import ca.gc.aafc.seqdb.api.validation.SequenceManagedAttributeValueValidator; @Service public class GenericMolecularAnalysisService extends DefaultDinaService { + private static final SequenceManagedAttributeValueValidator.SequenceManagedAttributeValidationContext + GENERIC_MOLECULAR_ANALYSIS_VALIDATION_CONTEXT = + SequenceManagedAttributeValueValidator.SequenceManagedAttributeValidationContext + .from(SequenceManagedAttribute.ManagedAttributeComponent.GENERIC_MOLECULAR_ANALYSIS); + + private final SequenceManagedAttributeValueValidator sequenceManagedAttributeValueValidator; + public GenericMolecularAnalysisService( - @NonNull BaseDAO baseDAO, @NonNull SmartValidator sv) { + @NonNull BaseDAO baseDAO, + @NonNull SequenceManagedAttributeValueValidator sequenceManagedAttributeValueValidator, + @NonNull SmartValidator sv) { super(baseDAO, sv); + + this.sequenceManagedAttributeValueValidator = sequenceManagedAttributeValueValidator; } @Override @@ -23,4 +37,17 @@ protected void preCreate(GenericMolecularAnalysis entity) { entity.setUuid(UUID.randomUUID()); } + @Override + public void validateBusinessRules(GenericMolecularAnalysis entity) { + validateManagedAttribute(entity); + } + + private void validateManagedAttribute(GenericMolecularAnalysis genericMolecularAnalysis) { + if (MapUtils.isNotEmpty(genericMolecularAnalysis.getManagedAttributes())) { + sequenceManagedAttributeValueValidator.validate(genericMolecularAnalysis, + genericMolecularAnalysis.getManagedAttributes(), + GENERIC_MOLECULAR_ANALYSIS_VALIDATION_CONTEXT); + } + } + } diff --git a/src/main/java/ca/gc/aafc/seqdb/api/util/ManagedAttributeIdMapper.java b/src/main/java/ca/gc/aafc/seqdb/api/util/ManagedAttributeIdMapper.java new file mode 100644 index 00000000..97417783 --- /dev/null +++ b/src/main/java/ca/gc/aafc/seqdb/api/util/ManagedAttributeIdMapper.java @@ -0,0 +1,31 @@ +package ca.gc.aafc.seqdb.api.util; + +import io.crnk.core.engine.parser.StringMapper; +import java.util.UUID; +import lombok.RequiredArgsConstructor; + +/** + * Lets you use either the UUID or the component type + key as the ID. + * e.g. /managed-attribute/generic_molecular_analysis.attribute_name. + */ +@RequiredArgsConstructor +public class ManagedAttributeIdMapper implements StringMapper { + private final StringMapper stringMapper; + + @Override + public Object parse(String input) { + // If the input's not in UUID format then use the raw string as the ID: + try { + UUID.fromString(input); + } catch (IllegalArgumentException e) { + return input; + } + return stringMapper.parse(input); + } + + @Override + public String toString(Object input) { + return stringMapper.toString(input); + } + +} diff --git a/src/main/java/ca/gc/aafc/seqdb/api/validation/SequenceManagedAttributeValueValidator.java b/src/main/java/ca/gc/aafc/seqdb/api/validation/SequenceManagedAttributeValueValidator.java new file mode 100644 index 00000000..7b2185f7 --- /dev/null +++ b/src/main/java/ca/gc/aafc/seqdb/api/validation/SequenceManagedAttributeValueValidator.java @@ -0,0 +1,110 @@ +package ca.gc.aafc.seqdb.api.validation; + +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; + +import ca.gc.aafc.dina.entity.DinaEntity; +import ca.gc.aafc.dina.service.ManagedAttributeService; +import ca.gc.aafc.dina.validation.ManagedAttributeValueValidator; +import ca.gc.aafc.dina.validation.ValidationContext; +import ca.gc.aafc.seqdb.api.entities.SequenceManagedAttribute; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import javax.inject.Named; +import lombok.NonNull; + +@Component +public class SequenceManagedAttributeValueValidator extends ManagedAttributeValueValidator { + + private static final String INVALID_VALIDATION_CONTEXT_KEY = "managedAttribute.validation.context.invalid"; + private static final String COMPONENT_FIELD_NAME = "managedAttributeComponent"; + + private final ManagedAttributeService dinaService; + private final MessageSource messageSource; + + public SequenceManagedAttributeValueValidator( + @Named("validationMessageSource") MessageSource baseMessageSource, // from dina-base + @NonNull MessageSource messageSource, + @NonNull ManagedAttributeService dinaService) { + super(baseMessageSource, dinaService); + this.dinaService = dinaService; + this.messageSource = messageSource; + } + + public void validate(D entity, Map managedAttributes, SequenceManagedAttributeValidationContext context) { + super.validate(entity, managedAttributes, context); + } + + /** + * override base class version to also add a restriction on the component since the uniqueness + * for CollectionManagedAttribute is key + component. + * @param keys + * @param validationContext + * @return + */ + @Override + protected Map findAttributesForValidation(Set keys, ValidationContext validationContext) { + return dinaService.findAttributesForKeys(keys, Pair.of(COMPONENT_FIELD_NAME, validationContext.getValue())); + } + + @Override + protected boolean preValidateValue(SequenceManagedAttribute managedAttributeDefinition, + String value, Errors errors, ValidationContext validationContext) { + + // expected context based on the component + SequenceManagedAttributeValidationContext expectedContext = + SequenceManagedAttributeValidationContext.from(managedAttributeDefinition.getManagedAttributeComponent()); + + if (!expectedContext.equals(validationContext)) { + errors.reject(INVALID_VALIDATION_CONTEXT_KEY, getMessageForKey(INVALID_VALIDATION_CONTEXT_KEY, + Objects.toString(validationContext), expectedContext.toString())); + return false; + } + return true; + } + + /** + * Wrapper class to expose {@link ca.gc.aafc.seqdb.api.entities.SequenceManagedAttribute.ManagedAttributeComponent} as + * {@link ValidationContext}. + */ + public static final class SequenceManagedAttributeValidationContext implements ValidationContext { + // make sure to only keep 1 instance per enum value + private static final EnumMap + INSTANCES = new EnumMap<>(SequenceManagedAttribute.ManagedAttributeComponent.class); + + private final SequenceManagedAttribute.ManagedAttributeComponent managedAttributeComponent; + + /** + * Use {@link #from(SequenceManagedAttribute.ManagedAttributeComponent)} method + * @param managedAttributeComponent + */ + private SequenceManagedAttributeValidationContext(SequenceManagedAttribute.ManagedAttributeComponent managedAttributeComponent) { + this.managedAttributeComponent = managedAttributeComponent; + } + + public static SequenceManagedAttributeValidationContext from(SequenceManagedAttribute.ManagedAttributeComponent managedAttributeComponent) { + return INSTANCES.computeIfAbsent(managedAttributeComponent, SequenceManagedAttributeValidationContext::new); + } + + @Override + public String toString() { + return managedAttributeComponent.toString(); + } + + @Override + public Object getValue() { + return managedAttributeComponent; + } + } + + // to be replaced by dina-base whne 0.132 will be released + private String getMessageForKey(String key, Object... objects) { + return messageSource.getMessage(key, objects, LocaleContextHolder.getLocale()); + } +} diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index e38fc4d8..8d58c493 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -60,4 +60,5 @@ + diff --git a/src/main/resources/db/changelog/migrations/56-Add_managed_attribute_to_molecular_analysis.xml b/src/main/resources/db/changelog/migrations/56-Add_managed_attribute_to_molecular_analysis.xml new file mode 100644 index 00000000..9710d649 --- /dev/null +++ b/src/main/resources/db/changelog/migrations/56-Add_managed_attribute_to_molecular_analysis.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/test/java/ca/gc/aafc/seqdb/api/repository/SequenceManagedAttributeRepositoryIT.java b/src/test/java/ca/gc/aafc/seqdb/api/repository/SequenceManagedAttributeRepositoryIT.java index 04cc5f02..983c1a66 100644 --- a/src/test/java/ca/gc/aafc/seqdb/api/repository/SequenceManagedAttributeRepositoryIT.java +++ b/src/test/java/ca/gc/aafc/seqdb/api/repository/SequenceManagedAttributeRepositoryIT.java @@ -51,4 +51,20 @@ void create_recordCreated() { SequenceManagedAttribute.ManagedAttributeComponent.GENERIC_MOLECULAR_ANALYSIS, result.getManagedAttributeComponent()); } + + @Test + @WithMockKeycloakUser(groupRole = SequenceManagedAttributeTestFixture.GROUP + ":SUPER_USER") + void findOneByKey_whenKeyProvided_managedAttributeFetched() { + SequenceManagedAttributeDto newAttribute = SequenceManagedAttributeTestFixture.newManagedAttribute(); + newAttribute.setName("Attribute 1"); + newAttribute.setVocabularyElementType(TypedVocabularyElement.VocabularyElementType.INTEGER); + newAttribute.setManagedAttributeComponent(SequenceManagedAttribute.ManagedAttributeComponent.GENERIC_MOLECULAR_ANALYSIS); + + UUID newAttributeUuid = repo.create(newAttribute).getUuid(); + + QuerySpec querySpec = new QuerySpec(SequenceManagedAttributeDto.class); + SequenceManagedAttributeDto fetchedAttribute = repo.findOne("generic_molecular_analysis.attribute_1", querySpec); + + assertEquals(newAttributeUuid, fetchedAttribute.getUuid()); + } } diff --git a/src/test/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisServiceIT.java b/src/test/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisServiceIT.java new file mode 100644 index 00000000..d57dd5ee --- /dev/null +++ b/src/test/java/ca/gc/aafc/seqdb/api/service/GenericMolecularAnalysisServiceIT.java @@ -0,0 +1,61 @@ +package ca.gc.aafc.seqdb.api.service; + +import org.junit.jupiter.api.Test; + +import ca.gc.aafc.dina.vocabulary.TypedVocabularyElement; +import ca.gc.aafc.seqdb.api.SequenceModuleBaseIT; +import ca.gc.aafc.seqdb.api.entities.GenericMolecularAnalysis; +import ca.gc.aafc.seqdb.api.entities.SequenceManagedAttribute; +import ca.gc.aafc.seqdb.api.testsupport.factories.GenericMolecularAnalysisFactory; +import ca.gc.aafc.seqdb.api.testsupport.factories.SequenceManagedAttributeFactory; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Map; +import javax.validation.ValidationException; + +public class GenericMolecularAnalysisServiceIT extends SequenceModuleBaseIT { + + private static final String GROUP = "grp"; + + @Test + void assignedValueContainedInAcceptedValues_validationPasses() { + SequenceManagedAttribute testManagedAttribute = SequenceManagedAttributeFactory.newManagedAttribute() + .acceptedValues(new String[]{"val1", "val2"}) + .managedAttributeComponent(SequenceManagedAttribute.ManagedAttributeComponent.GENERIC_MOLECULAR_ANALYSIS) + .build(); + + managedAttributeService.create(testManagedAttribute); + + GenericMolecularAnalysis genericMolecularAnalysis = GenericMolecularAnalysisFactory + .newGenericMolecularAnalysis() + .managedAttributes(Map.of(testManagedAttribute.getKey(), testManagedAttribute.getAcceptedValues()[0])) + .build(); + + assertDoesNotThrow(() -> genericMolecularAnalysisService.create(genericMolecularAnalysis)); + } + + @Test + void validate_WhenInvalidIntegerType_ExceptionThrown() { + SequenceManagedAttribute testManagedAttribute = + SequenceManagedAttributeFactory.newManagedAttribute() + .createdBy("GenericMolecularAnalysisServiceIT") + .managedAttributeComponent( + SequenceManagedAttribute.ManagedAttributeComponent.GENERIC_MOLECULAR_ANALYSIS) + .group(GROUP) + .vocabularyElementType(TypedVocabularyElement.VocabularyElementType.INTEGER) + .acceptedValues(null) + .build(); + + managedAttributeService.create(testManagedAttribute); + + GenericMolecularAnalysis genericMolecularAnalysis = GenericMolecularAnalysisFactory + .newGenericMolecularAnalysis() + .managedAttributes(Map.of(testManagedAttribute.getKey(), "1.2")) + .build(); + + assertThrows(ValidationException.class, + () -> genericMolecularAnalysisService.create(genericMolecularAnalysis)); + } +} \ No newline at end of file diff --git a/src/test/java/ca/gc/aafc/seqdb/api/service/SequenceManagedAttributeServiceIT.java b/src/test/java/ca/gc/aafc/seqdb/api/service/SequenceManagedAttributeServiceIT.java index 53372067..c4f9ce94 100644 --- a/src/test/java/ca/gc/aafc/seqdb/api/service/SequenceManagedAttributeServiceIT.java +++ b/src/test/java/ca/gc/aafc/seqdb/api/service/SequenceManagedAttributeServiceIT.java @@ -7,6 +7,7 @@ import ca.gc.aafc.seqdb.api.testsupport.factories.SequenceManagedAttributeFactory; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; public class SequenceManagedAttributeServiceIT extends SequenceModuleBaseIT { @@ -21,10 +22,10 @@ void delete_WhenNotInUse_DeleteAccepted() { managedAttributeService.findOne(attribute.getUuid(), SequenceManagedAttribute.class)); // To enable when usage is implemented -// managedAttributeService.delete(attribute); -// -// Assertions.assertNull( -// managedAttributeService.findOne(attribute.getUuid(), CollectionManagedAttribute.class)); + managedAttributeService.delete(attribute); + + assertNull( + managedAttributeService.findOne(attribute.getUuid(), SequenceManagedAttribute.class)); } private static SequenceManagedAttribute newAttribute(SequenceManagedAttribute.ManagedAttributeComponent component) {