diff --git a/.gitignore b/.gitignore index 03cba58e33e..f6962cfa2d7 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ Snap.* /database/ +/.run/ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index 82bc5752223..95c36644666 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -12,7 +12,7 @@ import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; import ca.uhn.fhir.jpa.graphql.GraphQLProviderWithIntrospection; import ca.uhn.fhir.jpa.provider.JpaSystemProvider; -import ca.uhn.fhir.jpa.provider.r4.IConsentExtensionProvider; +import ca.uhn.fhir.jpa.provider.r4.IMemberMatchConsentHook; import ca.uhn.fhir.jpa.provider.r4.MemberMatchR4ResourceProvider; import ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper; import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; @@ -105,7 +105,7 @@ public class JpaR4Config { @Autowired IFhirResourceDao theCoverageDao, @Autowired IFhirResourceDao thePatientDao, @Autowired IFhirResourceDao theConsentDao, - @Autowired(required = false) IConsentExtensionProvider theExtensionProvider + @Autowired(required = false) IMemberMatchConsentHook theExtensionProvider ) { return new MemberMatcherR4Helper( theContext, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IConsentExtensionProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IConsentExtensionProvider.java index b6795596395..b466b3ab0bf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IConsentExtensionProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IConsentExtensionProvider.java @@ -20,22 +20,34 @@ package ca.uhn.fhir.jpa.provider.r4; * #L% */ +import ca.uhn.fhir.util.ExtensionUtil; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collection; -import java.util.Collections; -public interface IConsentExtensionProvider { +/** + * Hook for Consent pre-save additions. + * + * @deprecated - we just use Consumer now + * TODO delete this. + */ +@Deprecated(since = "6.3.6", forRemoval = true) +public interface IConsentExtensionProvider extends IMemberMatchConsentHook { + Logger ourLog = LoggerFactory.getLogger(IConsentExtensionProvider.class); + + Collection getConsentExtension(IBaseResource theConsentResource); + + default void accept(IBaseResource theResource) { + Collection extensions = getConsentExtension(theResource); + + for (IBaseExtension ext : extensions) { + IBaseExtension e = ExtensionUtil.addExtension(theResource, ext.getUrl()); + e.setValue(ext.getValue()); + } + ourLog.trace("{} extension(s) added to Consent", extensions.size()); + } - /** - * Takes a Consent resource and returns a collection of Extensions that will - * be added to the base resource. - * - * @param theConsentResource - the consent resource - * @return - a collection of resources (or an empty collection if none). - */ - default Collection getConsentExtension(IBaseResource theConsentResource) { - return Collections.emptyList(); - }; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IMemberMatchConsentHook.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IMemberMatchConsentHook.java new file mode 100644 index 00000000000..921296a951f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/IMemberMatchConsentHook.java @@ -0,0 +1,11 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.function.Consumer; + +/** + * Pre-save hook for Consent saved during $member-match. + */ +public interface IMemberMatchConsentHook extends Consumer { +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatchR4ResourceProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatchR4ResourceProvider.java index c9de5423ea9..b1b95017850 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatchR4ResourceProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatchR4ResourceProvider.java @@ -34,7 +34,7 @@ import org.hl7.fhir.r4.model.Coverage; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; -import javax.servlet.http.HttpServletRequest; +import javax.annotation.Nullable; import java.util.Optional; public class MemberMatchR4ResourceProvider { @@ -76,32 +76,32 @@ public class MemberMatchR4ResourceProvider { RequestDetails theRequestDetails ) { - return doMemberMatchOperation(theServletRequest, theMemberPatient, oldCoverage, newCoverage, theConsent, theRequestDetails); + return doMemberMatchOperation(theMemberPatient, oldCoverage, newCoverage, theConsent, theRequestDetails); } - private Parameters doMemberMatchOperation(HttpServletRequest theServletRequest, Patient theMemberPatient, - Coverage theCoverageToMatch, Coverage theCoverageToLink, Consent theConsent, RequestDetails theRequestDetails) { + private Parameters doMemberMatchOperation(Patient theMemberPatient, + Coverage theCoverageToMatch, Coverage theCoverageToLink, Consent theConsent, RequestDetails theRequestDetails) { validateParams(theMemberPatient, theCoverageToMatch, theCoverageToLink, theConsent); - Optional coverageOpt = myMemberMatcherR4Helper.findMatchingCoverage(theCoverageToMatch); - if ( ! coverageOpt.isPresent()) { + Optional coverageOpt = myMemberMatcherR4Helper.findMatchingCoverage(theCoverageToMatch, theRequestDetails); + if (coverageOpt.isEmpty()) { String i18nMessage = myFhirContext.getLocalizer().getMessage( "operation.member.match.error.coverage.not.found"); throw new UnprocessableEntityException(Msg.code(1155) + i18nMessage); } Coverage coverage = coverageOpt.get(); - Optional patientOpt = myMemberMatcherR4Helper.getBeneficiaryPatient(coverage); - if (! patientOpt.isPresent()) { + Optional patientOpt = myMemberMatcherR4Helper.getBeneficiaryPatient(coverage, theRequestDetails); + if (patientOpt.isEmpty()) { String i18nMessage = myFhirContext.getLocalizer().getMessage( "operation.member.match.error.beneficiary.not.found"); throw new UnprocessableEntityException(Msg.code(1156) + i18nMessage); } Patient patient = patientOpt.get(); - if (!myMemberMatcherR4Helper.validPatientMember(patient, theMemberPatient)) { + if (!myMemberMatcherR4Helper.validPatientMember(patient, theMemberPatient, theRequestDetails)) { String i18nMessage = myFhirContext.getLocalizer().getMessage( "operation.member.match.error.patient.not.found"); throw new UnprocessableEntityException(Msg.code(2146) + i18nMessage); @@ -120,7 +120,7 @@ public class MemberMatchR4ResourceProvider { } myMemberMatcherR4Helper.addMemberIdentifierToMemberPatient(theMemberPatient, patient.getIdentifierFirstRep()); - myMemberMatcherR4Helper.updateConsentForMemberMatch(theConsent, patient, theMemberPatient); + myMemberMatcherR4Helper.updateConsentForMemberMatch(theConsent, patient, theMemberPatient, theRequestDetails); return myMemberMatcherR4Helper.buildSuccessReturnParameters(theMemberPatient, theCoverageToLink, theConsent); } @@ -133,7 +133,7 @@ public class MemberMatchR4ResourceProvider { validateConsentParam(theConsent); } - private void validateParam(Object theParam, String theParamName) { + private void validateParam(@Nullable Object theParam, String theParamName) { if (theParam == null) { String i18nMessage = myFhirContext.getLocalizer().getMessage( "operation.member.match.error.missing.parameter", theParamName); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4Helper.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4Helper.java index 817c6932afd..745ab0fab90 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4Helper.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4Helper.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.StringOrListParam; import ca.uhn.fhir.rest.param.StringParam; @@ -13,7 +14,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.ParametersUtil; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -21,7 +21,6 @@ import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Consent; import org.hl7.fhir.r4.model.Coverage; -import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; @@ -29,9 +28,9 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import javax.annotation.Nullable; -import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import static ca.uhn.fhir.rest.api.Constants.PARAM_CONSENT; import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_IDENTIFIER; @@ -59,7 +58,7 @@ import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE; */ public class MemberMatcherR4Helper { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MemberMatcherR4Helper.class); + static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MemberMatcherR4Helper.class); private static final String OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v2-0203"; private static final String OUT_COVERAGE_IDENTIFIER_CODE = "MB"; @@ -67,16 +66,14 @@ public class MemberMatcherR4Helper { private static final String COVERAGE_TYPE = "Coverage"; private static final String CONSENT_POLICY_REGULAR_TYPE = "regular"; private static final String CONSENT_POLICY_SENSITIVE_TYPE = "sensitive"; - public static final String CONSENT_IDENTIFIER_CODE_SYSTEM = "https://smilecdr.com/fhir/ns/member-match-fixme"; + public static final String CONSENT_IDENTIFIER_CODE_SYSTEM = "https://smilecdr.com/fhir/ns/member-match-source-client"; private final FhirContext myFhirContext; private final IFhirResourceDao myCoverageDao; private final IFhirResourceDao myPatientDao; private final IFhirResourceDao myConsentDao; - // by default, not provided - // but if it is, extensions can be added to Consent on $member-match - @Nullable - private final IConsentExtensionProvider myIConsentExtensionProvider; + /** A hook to modify the Consent before save */ + private final Consumer myConsentModifier; private boolean myRegularFilterSupported = false; @@ -85,27 +82,27 @@ public class MemberMatcherR4Helper { IFhirResourceDao theCoverageDao, IFhirResourceDao thePatientDao, IFhirResourceDao theConsentDao, - @Nullable IConsentExtensionProvider theExtensionProvider + @Nullable IMemberMatchConsentHook theConsentModifier ) { myFhirContext = theContext; myConsentDao = theConsentDao; myPatientDao = thePatientDao; myCoverageDao = theCoverageDao; - myIConsentExtensionProvider = theExtensionProvider; + myConsentModifier = (theConsentModifier != null) ? theConsentModifier : noop -> {}; } /** * Find Coverage matching the received member (Patient) by coverage id or by coverage identifier only */ - public Optional findMatchingCoverage(Coverage theCoverageToMatch) { + public Optional findMatchingCoverage(Coverage theCoverageToMatch, RequestDetails theRequestDetails) { // search by received old coverage id - List foundCoverages = findCoverageByCoverageId(theCoverageToMatch); + List foundCoverages = findCoverageByCoverageId(theCoverageToMatch, theRequestDetails); if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) { return Optional.of((Coverage) foundCoverages.get(0)); } // search by received old coverage identifier - foundCoverages = findCoverageByCoverageIdentifier(theCoverageToMatch); + foundCoverages = findCoverageByCoverageIdentifier(theCoverageToMatch, theRequestDetails); if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) { return Optional.of((Coverage) foundCoverages.get(0)); } @@ -114,7 +111,7 @@ public class MemberMatcherR4Helper { } - private List findCoverageByCoverageIdentifier(Coverage theCoverageToMatch) { + private List findCoverageByCoverageIdentifier(Coverage theCoverageToMatch, RequestDetails theRequestDetails) { TokenOrListParam identifierParam = new TokenOrListParam(); for (Identifier identifier : theCoverageToMatch.getIdentifier()) { identifierParam.add(identifier.getSystem(), identifier.getValue()); @@ -122,7 +119,7 @@ public class MemberMatcherR4Helper { SearchParameterMap paramMap = new SearchParameterMap() .add("identifier", identifierParam); - ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap); + ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap, theRequestDetails); return retVal.getAllResources(); } @@ -133,21 +130,23 @@ public class MemberMatcherR4Helper { } - private List findCoverageByCoverageId(Coverage theCoverageToMatch) { + private List findCoverageByCoverageId(Coverage theCoverageToMatch, RequestDetails theRequestDetails) { SearchParameterMap paramMap = new SearchParameterMap() .add("_id", new StringParam(theCoverageToMatch.getId())); - ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap); + ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap, theRequestDetails); return retVal.getAllResources(); } - public void updateConsentForMemberMatch(Consent theConsent, Patient thePatient, Patient theMemberPatient) { - addClientIdAsExtensionToConsentIfAvailable(theConsent); + public void updateConsentForMemberMatch(Consent theConsent, Patient thePatient, Patient theMemberPatient, RequestDetails theRequestDetails) { addIdentifierToConsent(theConsent, theMemberPatient); updateConsentPatientAndPerformer(theConsent, thePatient); + myConsentModifier.accept(theConsent); - // save the resource - myConsentDao.create(theConsent); + // WIPMB REVIEW QUESTION Which partition should we target? + // Will RequestTenantPartitionInterceptor or PatientIdPartitionInterceptor do the right thing? + // Can we use the userdata field to hint at target partition? + myConsentDao.create(theConsent, theRequestDetails); } public Parameters buildSuccessReturnParameters(Patient theMemberPatient, Coverage theCoverage, Consent theConsent) { @@ -160,24 +159,25 @@ public class MemberMatcherR4Helper { } private Identifier getIdentifier(Patient theMemberPatient) { - List memberIdentifiers = theMemberPatient.getIdentifier(); - if (memberIdentifiers != null && memberIdentifiers.size() > 0) { - for (Identifier memberIdentifier : memberIdentifiers) { - List typeCodings = memberIdentifier.getType().getCoding(); - if (memberIdentifier.getType() != null && typeCodings != null && typeCodings.size() > 0) { - for (Coding typeCoding : typeCodings) { - if (typeCoding.getCode().equals("MB")) { - return memberIdentifier; - } - } - } - } - } - String i18nMessage = myFhirContext.getLocalizer().getMessage( - "operation.member.match.error.beneficiary.without.identifier"); - throw new UnprocessableEntityException(Msg.code(2219) + i18nMessage); + return theMemberPatient.getIdentifier() + .stream() + .filter(this::isTypeMB) + .findFirst() + .orElseThrow(()->{ + String i18nMessage = myFhirContext.getLocalizer().getMessage( + "operation.member.match.error.beneficiary.without.identifier"); + return new UnprocessableEntityException(Msg.code(2219) + i18nMessage); + }); } + private boolean isTypeMB(Identifier theMemberIdentifier) { + return theMemberIdentifier.getType() != null && + theMemberIdentifier.getType().getCoding() + .stream() + .anyMatch(typeCoding->typeCoding.getCode().equals("MB")); + } + + public void addMemberIdentifierToMemberPatient(Patient theMemberPatient, Identifier theNewIdentifier) { Coding coding = new Coding() .setSystem(OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM) @@ -198,31 +198,7 @@ public class MemberMatcherR4Helper { theMemberPatient.addIdentifier(newIdentifier); } - /** - * If there is a client id - * - * @param theConsent - the consent to modify - */ - private void addClientIdAsExtensionToConsentIfAvailable(Consent theConsent) { - if (myIConsentExtensionProvider != null) { - Collection extensions = myIConsentExtensionProvider.getConsentExtension(theConsent); - - for (IBaseExtension ext : extensions) { - if (ext instanceof Extension) { - theConsent.addExtension((Extension) ext); - } else { - Extension extR4 = new Extension(); - extR4.setUrl(ext.getUrl()); - extR4.setValue(ext.getValue()); - theConsent.addExtension(extR4); - } - } - - ourLog.trace("{} extension(s) added to Consent", extensions.size()); - } - } - - public Optional getBeneficiaryPatient(Coverage theCoverage) { + public Optional getBeneficiaryPatient(Coverage theCoverage, RequestDetails theRequestDetails) { if (theCoverage.getBeneficiaryTarget() == null && theCoverage.getBeneficiary() == null) { return Optional.empty(); } @@ -245,15 +221,15 @@ public class MemberMatcherR4Helper { return Optional.empty(); } - Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference())); + Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference()), theRequestDetails); return Optional.ofNullable(beneficiary); } /** * Matching by member patient demographics - family name and birthdate only */ - public boolean validPatientMember(Patient thePatientFromContract, Patient thePatientToMatch) { - if (thePatientFromContract == null || thePatientFromContract.getIdElement() == null) { + public boolean validPatientMember(Patient thePatientFromContract, Patient thePatientToMatch, RequestDetails theRequestDetails) { + if (thePatientFromContract == null || thePatientFromContract.getIdElement() == null || thePatientToMatch == null) { return false; } StringOrListParam familyName = new StringOrListParam(); @@ -263,7 +239,7 @@ public class MemberMatcherR4Helper { SearchParameterMap map = new SearchParameterMap() .add("family", familyName) .add("birthdate", new DateParam(thePatientToMatch.getBirthDateElement().getValueAsString())); - ca.uhn.fhir.rest.api.server.IBundleProvider bundle = myPatientDao.search(map); + ca.uhn.fhir.rest.api.server.IBundleProvider bundle = myPatientDao.search(map, theRequestDetails); for (IBaseResource patientResource : bundle.getAllResources()) { IIdType patientId = patientResource.getIdElement().toUnqualifiedVersionless(); if (patientId.getValue().equals(thePatientFromContract.getIdElement().toUnqualifiedVersionless().getValue())) { @@ -286,18 +262,16 @@ public class MemberMatcherR4Helper { } /** - * This is filtering the Consent policy data. The rule is specified in - * https://build.fhir.org/ig/HL7/davinci-ehrx/StructureDefinition-hrex-consent.html#notes + * The consent policy rules are + * + * described here. */ private boolean validConsentPolicy(String thePolicyUri) { String policyTypes = StringUtils.substringAfterLast(thePolicyUri, "#"); if (policyTypes.equals(CONSENT_POLICY_SENSITIVE_TYPE)) { return true; } - if (policyTypes.equals(CONSENT_POLICY_REGULAR_TYPE) && myRegularFilterSupported) { - return true; - } - return false; + return policyTypes.equals(CONSENT_POLICY_REGULAR_TYPE) && myRegularFilterSupported; } private void addIdentifierToConsent(Consent theConsent, Patient thePatient) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/MemberMatcherR4HelperTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4HelperTest.java similarity index 79% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/MemberMatcherR4HelperTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4HelperTest.java index d0730c71f88..8b091467cab 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/MemberMatcherR4HelperTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MemberMatcherR4HelperTest.java @@ -1,64 +1,60 @@ -package ca.uhn.fhir.jpa.provider; +package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.provider.r4.IConsentExtensionProvider; -import ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.SimpleBundleProvider; +import ca.uhn.test.util.LogbackCaptureTestExtension; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; import com.google.common.collect.Lists; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Consent; import org.hl7.fhir.r4.model.Coverage; import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.StringType; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.List; import java.util.Optional; -import static ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper.CONSENT_IDENTIFIER_CODE_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.PARAM_CONSENT; import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_IDENTIFIER; import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_PATIENT; import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.Mockito.never; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -66,11 +62,8 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class MemberMatcherR4HelperTest { - private static final Logger ourLog = (Logger) LoggerFactory.getLogger(MemberMatcherR4Helper.class); - - @Mock - private ListAppender myAppender; - + @RegisterExtension + LogbackCaptureTestExtension myLogCapture = new LogbackCaptureTestExtension((Logger) MemberMatcherR4Helper.ourLog, Level.TRACE); @Spy private final FhirContext myFhirContext = FhirContext.forR4(); @Mock @@ -81,6 +74,7 @@ public class MemberMatcherR4HelperTest { private IFhirResourceDao myConsentDao; private MemberMatcherR4Helper myHelper; + RequestDetails myRequestDetails = new SystemRequestDetails(); @BeforeEach public void before() { @@ -91,13 +85,6 @@ public class MemberMatcherR4HelperTest { myConsentDao, null // extension provider ); - - ourLog.addAppender(myAppender); - } - - @AfterEach - public void after() { - ourLog.detachAppender(myAppender); } @Mock private Coverage myCoverageToMatch; @@ -114,13 +101,13 @@ public class MemberMatcherR4HelperTest { @Test void findMatchingCoverageMatchByIdReturnsMatched() { when(myCoverageToMatch.getId()).thenReturn("cvg-to-match-id"); - when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider); + when(myCoverageDao.search(isA(SearchParameterMap.class), same(myRequestDetails))).thenReturn(myBundleProvider); when(myBundleProvider.getAllResources()).thenReturn(Collections.singletonList(myMatchedCoverage)); - Optional result = myHelper.findMatchingCoverage(myCoverageToMatch); + Optional result = myHelper.findMatchingCoverage(myCoverageToMatch, myRequestDetails); assertEquals(Optional.of(myMatchedCoverage), result); - verify(myCoverageDao).search(mySearchParameterMapCaptor.capture()); + verify(myCoverageDao).search(mySearchParameterMapCaptor.capture(), same(myRequestDetails)); SearchParameterMap spMap = mySearchParameterMapCaptor.getValue(); assertTrue(spMap.containsKey("_id")); List> listListParams = spMap.get("_id"); @@ -135,14 +122,14 @@ public class MemberMatcherR4HelperTest { void findMatchingCoverageMatchByIdentifierReturnsMatched() { when(myCoverageToMatch.getId()).thenReturn("non-matching-id"); when(myCoverageToMatch.getIdentifier()).thenReturn(Collections.singletonList(myMatchingIdentifier)); - when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider); + when(myCoverageDao.search(isA(SearchParameterMap.class), same(myRequestDetails))).thenReturn(myBundleProvider); when(myBundleProvider.getAllResources()).thenReturn( Collections.emptyList(), Collections.singletonList(myMatchedCoverage)); - Optional result = myHelper.findMatchingCoverage(myCoverageToMatch); + Optional result = myHelper.findMatchingCoverage(myCoverageToMatch, myRequestDetails); assertEquals(Optional.of(myMatchedCoverage), result); - verify(myCoverageDao, times(2)).search(mySearchParameterMapCaptor.capture()); + verify(myCoverageDao, times(2)).search(mySearchParameterMapCaptor.capture(), same(myRequestDetails)); List spMap = mySearchParameterMapCaptor.getAllValues(); assertTrue(spMap.get(0).containsKey("_id")); assertTrue(spMap.get(1).containsKey("identifier")); @@ -159,10 +146,10 @@ public class MemberMatcherR4HelperTest { void findMatchingCoverageNoMatchReturnsEmpty() { when(myCoverageToMatch.getId()).thenReturn("non-matching-id"); when(myCoverageToMatch.getIdentifier()).thenReturn(Collections.singletonList(myMatchingIdentifier)); - when(myCoverageDao.search(any(SearchParameterMap.class))).thenReturn(myBundleProvider); + when(myCoverageDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenReturn(myBundleProvider); when(myBundleProvider.getAllResources()).thenReturn(Collections.emptyList(), Collections.emptyList()); - Optional result = myHelper.findMatchingCoverage(myCoverageToMatch); + Optional result = myHelper.findMatchingCoverage(myCoverageToMatch, myRequestDetails); assertFalse(result.isPresent()); } @@ -245,7 +232,7 @@ public class MemberMatcherR4HelperTest { when(coverage.getBeneficiaryTarget()).thenReturn(null); when(coverage.getBeneficiary()).thenReturn(null); - Optional result = myHelper.getBeneficiaryPatient(coverage); + Optional result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails); assertFalse(result.isPresent()); } @@ -256,7 +243,7 @@ public class MemberMatcherR4HelperTest { when(coverage.getBeneficiary()).thenReturn(null); when(coverage.getBeneficiaryTarget()).thenReturn(new Patient()); - Optional result = myHelper.getBeneficiaryPatient(coverage); + Optional result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails); assertFalse(result.isPresent()); } @@ -267,7 +254,7 @@ public class MemberMatcherR4HelperTest { Patient patient = new Patient().setIdentifier(Collections.singletonList(new Identifier())); when(coverage.getBeneficiaryTarget()).thenReturn(patient); - Optional result = myHelper.getBeneficiaryPatient(coverage); + Optional result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails); assertTrue(result.isPresent()); assertEquals(patient, result.get()); @@ -280,7 +267,7 @@ public class MemberMatcherR4HelperTest { when(coverage.getBeneficiaryTarget()).thenReturn(null); when(coverage.getBeneficiary().getResource()).thenReturn(patient); - Optional result = myHelper.getBeneficiaryPatient(coverage); + Optional result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails); assertTrue(result.isPresent()); assertEquals(patient, result.get()); @@ -292,7 +279,7 @@ public class MemberMatcherR4HelperTest { when(coverage.getBeneficiaryTarget()).thenReturn(null); when(coverage.getBeneficiary()).thenReturn(new Reference()); - Optional result = myHelper.getBeneficiaryPatient(coverage); + Optional result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails); assertFalse(result.isPresent()); } @@ -304,11 +291,10 @@ public class MemberMatcherR4HelperTest { when(coverage.getBeneficiary().getResource()).thenReturn(null); when(coverage.getBeneficiary().getReference()).thenReturn("patient-id"); - myHelper.getBeneficiaryPatient(coverage); + myHelper.getBeneficiaryPatient(coverage, myRequestDetails); - verify(myPatientDao).read(new IdDt("patient-id")); + verify(myPatientDao).read(new IdDt("patient-id"), myRequestDetails); } - } /** @@ -317,19 +303,17 @@ public class MemberMatcherR4HelperTest { @Nested public class TestValidPatientMember { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Coverage coverage; - private Patient patient; + private final Patient patient = new Patient(); @Test void noPatientFoundFromContractReturnsFalse() { - boolean result = myHelper.validPatientMember(null, patient); + boolean result = myHelper.validPatientMember(null, patient, myRequestDetails); assertFalse(result); } @Test void noPatientFoundFromPatientMemberReturnsFalse() { - boolean result = myHelper.validPatientMember(patient, null); + boolean result = myHelper.validPatientMember(patient, null, myRequestDetails); assertFalse(result); } @@ -337,11 +321,11 @@ public class MemberMatcherR4HelperTest { void noMatchingFamilyNameReturnsFalse() { Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "2020-01-01"); Patient patientFromContractFound = getPatientWithIDParm("A123", "Smith", "2020-01-01"); - when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> { + when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> { IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123"))); return provider; }); - boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch); + boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails); assertFalse(result); } @@ -350,11 +334,11 @@ public class MemberMatcherR4HelperTest { void noMatchingBirthdayReturnsFalse() { Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "1990-01-01"); Patient patientFromContractFound = getPatientWithIDParm("A123", "Person", "2020-01-01"); - when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> { + when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> { IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123"))); return provider; }); - boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch); + boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails); assertFalse(result); } @@ -362,11 +346,11 @@ public class MemberMatcherR4HelperTest { void noMatchingFieldsReturnsFalse() { Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "1990-01-01"); Patient patientFromContractFound = getPatientWithIDParm("A123", "Smith", "2020-01-01"); - when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> { + when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> { IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123"))); return provider; }); - boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch); + boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails); assertFalse(result); } @@ -374,11 +358,11 @@ public class MemberMatcherR4HelperTest { void patientMatchingReturnTrue() { Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "2020-01-01"); Patient patientFromContractFound = getPatientWithIDParm("A123", "Person", "2020-01-01"); - when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> { + when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> { IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(patientFromContractFound)); return provider; }); - boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch); + boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails); assertTrue(result); } @@ -404,9 +388,6 @@ public class MemberMatcherR4HelperTest { @Nested public class TestValidvalidConsentDataAccess { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Coverage coverage; - private Patient patient; private Consent consent; @Test @@ -521,45 +502,10 @@ public class MemberMatcherR4HelperTest { return patient; } - private void verifyConsentUpdatedAfterMemberMatch( - Consent theConsent, - Patient thePatient, - Patient theMemberPatient, - List theSavedExtensions - ) { - // check consent identifier - assertEquals(1, theConsent.getIdentifier().size()); - assertEquals(CONSENT_IDENTIFIER_CODE_SYSTEM, theConsent.getIdentifier().get(0).getSystem()); - assertNotNull(theConsent.getIdentifier().get(0).getValue()); - assertEquals(theConsent.getIdentifier().get(0).getValue(), theMemberPatient.getIdentifier().get(0).getValue()); - - // check consent patient info - String patientRef = thePatient.getIdElement().toUnqualifiedVersionless().getValue(); - assertEquals(patientRef, theConsent.getPatient().getReference()); - assertEquals(patientRef, theConsent.getPerformer().get(0).getReference()); - - if (!theSavedExtensions.isEmpty()) { - // check consent extensions - assertNotNull(theConsent.getExtension()); - assertEquals(theSavedExtensions.size(), theConsent.getExtension().size()); - for (Extension ext : theSavedExtensions) { - boolean found = false; - for (Extension consentExt : theConsent.getExtension()) { - if (consentExt.getUrl().equals(ext.getUrl()) - && consentExt.getValue().equals(ext.getValue())) { - found = true; - break; - } - } - assertTrue(found, - "Extension " + ext.getUrl() + "|" + ext.getValue().toString() + " not found" - ); - } - } - } - @Nested public class MemberMatchWithoutConsentProvider { + @Captor + private ArgumentCaptor myDaoCaptor; @Test public void updateConsentForMemberMatch_noProvider_addsIdentifierUpdatePatientButNotExtensionAndSaves() { @@ -569,19 +515,26 @@ public class MemberMatcherR4HelperTest { Patient patient = createPatientForMemberMatchUpdate(false); Patient memberPatient = createPatientForMemberMatchUpdate(true); - ourLog.setLevel(Level.TRACE); - // test - myHelper.updateConsentForMemberMatch(consent, patient, memberPatient); + myHelper.updateConsentForMemberMatch(consent, patient, memberPatient, myRequestDetails); // verify - verify(myAppender, never()) - .doAppend(any(ILoggingEvent.class)); - ArgumentCaptor consentCaptor = ArgumentCaptor.forClass(Consent.class); - verify(myConsentDao).create(consentCaptor.capture()); - Consent saved = consentCaptor.getValue(); - verifyConsentUpdatedAfterMemberMatch(saved, patient, memberPatient, Collections.emptyList()); + verify(myConsentDao).create(myDaoCaptor.capture(), same(myRequestDetails)); + Consent saved = myDaoCaptor.getValue(); + // check consent identifier + assertEquals(1, saved.getIdentifier().size()); + assertEquals(MemberMatcherR4Helper.CONSENT_IDENTIFIER_CODE_SYSTEM, saved.getIdentifier().get(0).getSystem()); + assertNotNull(saved.getIdentifier().get(0).getValue()); + assertEquals(saved.getIdentifier().get(0).getValue(), memberPatient.getIdentifier().get(0).getValue()); + + // check consent patient info + String patientRef = patient.getIdElement().toUnqualifiedVersionless().getValue(); + assertEquals(patientRef, saved.getPatient().getReference()); + assertEquals(patientRef, saved.getPerformer().get(0).getReference()); + + assertThat(myLogCapture.getLogEvents(), empty()); + } } @@ -589,6 +542,8 @@ public class MemberMatcherR4HelperTest { public class MemberMatchWithConsentProvider { @Mock private IConsentExtensionProvider myExtensionProvider; + @Captor + private ArgumentCaptor myHookCaptor; @BeforeEach public void before() { @@ -607,31 +562,17 @@ public class MemberMatcherR4HelperTest { Consent consent = getConsent(); consent.addPolicy(constructConsentPolicyComponent("#sensitive")); consent.setId("Consent/RED"); - Extension ext = new Extension(); - ext.setUrl("http://example.com"); - ext.setValue(new StringType("value")); Patient patient = createPatientForMemberMatchUpdate(false); Patient memberPatient = createPatientForMemberMatchUpdate(true); - ourLog.setLevel(Level.TRACE); - - // when - when(myExtensionProvider.getConsentExtension(any(IBaseResource.class))) - .thenReturn(Collections.singleton(ext)); - // test - myHelper.updateConsentForMemberMatch(consent, patient, memberPatient); + myHelper.updateConsentForMemberMatch(consent, patient, memberPatient, myRequestDetails); // verify - ArgumentCaptor consentCaptor = ArgumentCaptor.forClass(Consent.class); - verify(myConsentDao).create(consentCaptor.capture()); - Consent saved = consentCaptor.getValue(); - verifyConsentUpdatedAfterMemberMatch(saved, patient, memberPatient, Collections.emptyList()); + verify(myExtensionProvider).accept(myHookCaptor.capture()); - ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(ILoggingEvent.class); - verify(myAppender).doAppend(eventCaptor.capture()); - ILoggingEvent event = eventCaptor.getValue(); - assertEquals("1 extension(s) added to Consent", event.getFormattedMessage()); + assertSame(consent, myHookCaptor.getValue()); + assertThat(myLogCapture.getLogEvents(), empty()); } } } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/LogbackCaptureTestExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/LogbackCaptureTestExtension.java index 1fb2b5b9631..d115e3bb9fc 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/LogbackCaptureTestExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/LogbackCaptureTestExtension.java @@ -113,13 +113,13 @@ public class LogbackCaptureTestExtension implements BeforeEachCallback, AfterEac * Guts of beforeEach exposed for manual lifecycle. */ public void setUp() { + myListAppender = new ListAppender<>(); + myListAppender.start(); + myLogger.addAppender(myListAppender); if (myLevel != null) { mySavedLevel = myLogger.getLevel(); myLogger.setLevel(myLevel); } - myListAppender = new ListAppender<>(); - myListAppender.start(); - myLogger.addAppender(myListAppender); } @Override