This commit is contained in:
James Agnew 2024-11-14 08:42:57 -05:00
parent c79a705124
commit 91f2c8f526
8 changed files with 137 additions and 59 deletions

View File

@ -330,7 +330,7 @@ public class AuthorizationInterceptors {
SearchNarrowingConsentService consentService =
new SearchNarrowingConsentService(validationSupport, searchParamRegistry);
// Create a ConsentIntereptor to apply the ConsentService and register it with the server
// Create a ConsentInterceptor to apply the ConsentService and register it with the server
ConsentInterceptor consentInterceptor = new ConsentInterceptor();
consentInterceptor.registerConsentService(consentService);
restfulServer.registerInterceptor(consentInterceptor);

View File

@ -7,7 +7,7 @@
title: "Remove a dependency on a Java 1.7 class (ReflectiveOperationException) in several spots in the codebase. This dependency was accidentally introduced in 1.3, and animal-sniffer-plugin failed to detect it (sigh)."
- item:
type: "add"
title: "Add two new server interceptors: RequestValidatingInterceptor and ResponseValidatingInterceptor which can be used to validate incoming requests or outgoing responses using the standard FHIR validation tools. See the Server Validation Page for examples of how to use these interceptors. These intereptors have both been enabled on the <a href=\"http://fhirtest.uhn.ca\">public test page</a>."
title: "Add two new server interceptors: RequestValidatingInterceptor and ResponseValidatingInterceptor which can be used to validate incoming requests or outgoing responses using the standard FHIR validation tools. See the Server Validation Page for examples of how to use these interceptors. These interceptors have both been enabled on the <a href=\"http://fhirtest.uhn.ca\">public test page</a>."
- item:
issue: "259"
type: "fix"

View File

@ -32,7 +32,7 @@
title: "<b>New Feature</b>: The JPA server now supports the <code>_filter</code> search parameter when configured to do so. The <a href=\"http://hl7.org/fhir/search_filter.html\">filter search parameter</a> is an extremely flexible and powerful feature, allowing for advanced grouping and order of operations on searches. It can be dangerous however, as it potentially allows users to create queries for which no database indexes exist in the default configuration so it is disabled by default. Thanks to Anthony Sute for the pull request and all of his support in what turned out to be a lengthy merge!"
- item:
type: "add"
title: "<b>New Feature</b>: A new interceptor called CascadingDeleteInterceptor has been added to the JPA project. This interceptor allows deletes to cascade when a specific URL parameter or header is added to the request. Cascading deletes can also be controlled by a new flag in the AuthorizationIntereptor RuleBuilder, in order to ensure that cascading deletes are only available to users with sufficient permission."
title: "<b>New Feature</b>: A new interceptor called CascadingDeleteInterceptor has been added to the JPA project. This interceptor allows deletes to cascade when a specific URL parameter or header is added to the request. Cascading deletes can also be controlled by a new flag in the AuthorizationInterceptor RuleBuilder, in order to ensure that cascading deletes are only available to users with sufficient permission."
- item:
type: "add"
title: "Several enhancements have been made to the <code>AuthorizationInterceptor</code> : <ul> <li>The interceptor now registers against the <code>STORAGE_PRESHOW_RESOURCES</code> interceptor hook, which allows it to successfully authorize JPA operations that don't actually return resource content, such as GraphQL responses, and resources that have been filtered using the <code>_elements</code> parameter.</li> <li> </li>The rule list is now cached on a per-request basis, which should improve performance</ul>"

View File

@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -35,6 +36,9 @@ import static org.assertj.core.api.Assertions.assertThat;
@Service
public class MdmLinkHelper {
public static final String SERVER_ASSIGNED_PREFIX = "SERVER_ASSIGNED_";
private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkHelper.class);
private enum Side {
@ -43,16 +47,11 @@ public class MdmLinkHelper {
}
@Autowired
private IMdmLinkDao myMdmLinkRepo;
private IMdmLinkDao<JpaPid, MdmLink> myMdmLinkRepo;
@Autowired
private IFhirResourceDao<Patient> myPatientDao;
@Autowired
private MdmLinkDaoSvc<JpaPid, MdmLink> myMdmLinkDaoSvc;
@SuppressWarnings("rawtypes")
@Autowired
private IMdmLinkDao<JpaPid, MdmLink> myMdmLinkDao;
@Autowired
private IdHelperService myIdHelperService;
@Transactional
public void logMdmLinks() {
@ -91,7 +90,7 @@ public class MdmLinkHelper {
}
// create all the links
for (MdmTestLinkExpression inputExpression : theState.getParsedInputState()) {
for (MdmTestLinkExpression inputExpression : inputExpressions) {
ourLog.info(inputExpression.getLinkExpression());
Patient goldenResource = theState.getParameter(inputExpression.getLeftSideResourceIdentifier());
@ -106,7 +105,7 @@ public class MdmLinkHelper {
);
matchOutcome.setMatchResultEnum(matchResultType);
MdmLink link = (MdmLink) myMdmLinkDaoSvc.createOrUpdateLinkEntity(
MdmLink link = myMdmLinkDaoSvc.createOrUpdateLinkEntity(
goldenResource, // golden
targetResource, // source
matchOutcome, // match outcome
@ -130,13 +129,21 @@ public class MdmLinkHelper {
}
private Patient createPatientAndTags(String theId, MDMState<Patient, JpaPid> theState) {
boolean clientAssignedId = theId.startsWith(SERVER_ASSIGNED_PREFIX);
boolean previouslyExisting = false;
Patient patient = new Patient();
patient.setActive(true); // all mdm patients must be active
// we add an identifier and use a forced id
// to make test debugging a little simpler
patient.addIdentifier(new Identifier().setValue(theId));
patient.setId(theId);
if (clientAssignedId && theState.getForcedIdForConditionalIdPlaceholder(theId) != null) {
patient.setId(theState.getForcedIdForConditionalIdPlaceholder(theId));
previouslyExisting = true;
} else if (!clientAssignedId) {
patient.setId(theId);
}
// Golden patients will be "GP#"
if (theId.length() >= 2 && theId.charAt(0) == 'G') {
@ -145,10 +152,20 @@ public class MdmLinkHelper {
}
MdmResourceUtil.setMdmManaged(patient);
DaoMethodOutcome outcome = myPatientDao.update(patient,
SystemRequestDetails.forAllPartitions());
SystemRequestDetails srd = SystemRequestDetails.forAllPartitions();
DaoMethodOutcome outcome;
if (clientAssignedId && !previouslyExisting) {
outcome = myPatientDao.create(patient, srd);
} else {
outcome = myPatientDao.update(patient, srd);
}
Patient outputPatient = (Patient) outcome.getResource();
theState.addPID(theId, (JpaPid) outcome.getPersistentId());
if (clientAssignedId) {
theState.addConditionalIdPlaceholderToForcedId(theId, outputPatient.getIdPart());
}
return outputPatient;
}
@ -209,9 +226,7 @@ public class MdmLinkHelper {
}
public List<MdmLink> getAllMdmLinks(Patient theGoldenPatient) {
return myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient).stream()
.map( link -> (MdmLink) link)
.collect(Collectors.toList());
return new ArrayList<>(myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient));
}
private boolean isResourcePartOfLink(

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.mdm.helper.testmodels;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import org.testcontainers.shaded.com.google.common.collect.HashMultimap;
import org.testcontainers.shaded.com.google.common.collect.Multimap;
@ -30,6 +31,7 @@ public class MDMState<T, P extends IResourcePersistentId> {
* Eg:
* PG1, MANUAL, MATCH, P1
* PG1, AUTO, POSSIBLE_MATCH, P2
* SERVER_ASSIGNED_GP, AUTO, POSSIBLE_MATCH, SERVER_ASSIGNED_P2
*/
private String myInputState;
@ -59,16 +61,37 @@ public class MDMState<T, P extends IResourcePersistentId> {
* Map of forcedId -> resource persistent id for each resource created
*/
private final Map<String, P> myForcedIdToPID = new HashMap<>();
private final Map<P, String> myPIDToForcedId = new HashMap<>();
private final Map<String, String> myConditionalIdPlaceholderToForcedId = new HashMap<>();
public void addPID(String theForcedId, P thePid) {
assert !myForcedIdToPID.containsKey(theForcedId);
myForcedIdToPID.put(theForcedId, thePid);
myPIDToForcedId.put(thePid, theForcedId);
}
public P getPID(String theForcedId) {
return myForcedIdToPID.get(theForcedId);
}
public String getForcedId(JpaPid thePID) {
String retVal = myPIDToForcedId.get(thePID);
if (myConditionalIdPlaceholderToForcedId.containsKey(retVal)) {
retVal = myConditionalIdPlaceholderToForcedId.get(retVal);
}
return retVal;
}
public void addConditionalIdPlaceholderToForcedId(String theConditionalIdPlaceholder, String theForcedId) {
assert !myConditionalIdPlaceholderToForcedId.containsKey(theConditionalIdPlaceholder);
myConditionalIdPlaceholderToForcedId.put(theConditionalIdPlaceholder, theForcedId);
}
public String getForcedIdForConditionalIdPlaceholder(String theConditionalIdPlaceholder) {
return myConditionalIdPlaceholderToForcedId.get(theConditionalIdPlaceholder);
}
public Multimap<T, MdmLink> getActualOutcomeLinks() {
if (myActualOutcomeLinks == null) {
myActualOutcomeLinks = HashMultimap.create();
@ -111,6 +134,18 @@ public class MDMState<T, P extends IResourcePersistentId> {
return myInputState;
}
/**
* Initial state for test.
* Comma separated lines with:
* Left param value, MdmLinkSourceEnum value, MdmMatchResultEnum value, Right param value
*
* Each input line represents a link state
*
* Eg:
* PG1, MANUAL, MATCH, P1
* PG1, AUTO, POSSIBLE_MATCH, P2
* SERVER_ASSIGNED_GP, AUTO, POSSIBLE_MATCH, SERVER_ASSIGNED_P2
*/
public MDMState<T, P> setInputState(String theInputState) {
myInputState = theInputState;
return this;

View File

@ -1,14 +1,24 @@
package ca.uhn.fhir.jpa.mdm.interceptor;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.IPointcut;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper;
import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMLinkResults;
import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMState;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.mdm.interceptor.MdmReadVirtualizationInterceptor;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -24,6 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -39,6 +50,8 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
public MdmHelperR4 myMdmHelper;
@Autowired
private MdmReadVirtualizationInterceptor<JpaPid> myInterceptor;
@Autowired
private MdmLinkHelper myLinkHelper;
private IIdType mySourcePatientA0Id;
private IIdType myGoldenResourcePatientAId;
@ -49,7 +62,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
private IIdType myObservationReferencingSourcePatientA2Id;
private IIdType myObservationReferencingGoldenPatientAId;
private IIdType mySourcePatientB0Id;
private IdType myGoldenResourcePatientBId;
private IIdType myGoldenResourcePatientBId;
private IIdType myObservationReferencingSourcePatientB0Id;
@Override
@ -63,6 +76,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
public void after() throws IOException {
super.after();
myInterceptorRegistry.unregisterInterceptor(myInterceptor);
myInterceptorRegistry.unregisterAllAnonymousInterceptors();
}
/**
@ -71,7 +85,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
*/
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testRead_ObservationReferencingSourcePatient(boolean theUseClientAssignedIds) throws InterruptedException {
public void testRead_ObservationReferencingSourcePatient(boolean theUseClientAssignedIds) {
// Setup
createTestData(theUseClientAssignedIds);
registerVirtualizationInterceptor();
@ -89,7 +103,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
*/
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testRead_ObservationReferencingGoldenPatient(boolean theUseClientAssignedIds) throws InterruptedException {
public void testRead_ObservationReferencingGoldenPatient(boolean theUseClientAssignedIds) {
// Setup
createTestData(theUseClientAssignedIds);
registerVirtualizationInterceptor();
@ -105,7 +119,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
* If we search for all patients, only the golden resource ones should be returned
*/
@Test
public void testSearch_Patient_FetchAll() throws InterruptedException {
public void testSearch_Patient_FetchAll() {
// Setup
createTestData(false);
registerVirtualizationInterceptor();
@ -123,7 +137,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
* golden patients
*/
@Test
public void testSearch_Patient_FetchOnlySource() throws InterruptedException {
public void testSearch_Patient_FetchOnlySource() {
// Setup
createTestData(false);
registerVirtualizationInterceptor();
@ -144,10 +158,11 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
* If we search for all patients and _revinclude things that point to them,
* only the golden resource ones should be returned
*/
@Test
public void testSearch_Patient_FetchAll_AlsoRevIncludeDependentResources() throws InterruptedException {
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testSearch_Patient_FetchAll_AlsoRevIncludeDependentResources(boolean theUseClientAssginedId) {
// Setup
createTestData(false);
createTestData(theUseClientAssginedId);
registerVirtualizationInterceptor();
// Test
@ -175,7 +190,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
}
@Test
public void testSearch_Observation_SpecificSourcePatient() throws InterruptedException {
public void testSearch_Observation_SpecificSourcePatient() {
// Setup
createTestData(false);
registerVirtualizationInterceptor();
@ -200,47 +215,59 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
assertEquals(myGoldenResourcePatientAId.getValue(), obs.getSubject().getReference());
}
private void registerVirtualizationInterceptor() {
myInterceptorRegistry.registerInterceptor(myInterceptor);
}
private void createTestData(boolean theUseClientAssignedIds) throws InterruptedException {
MdmHelperR4.OutcomeAndLogMessageWrapper createPatientOutcome;
private void createTestData(boolean theUseClientAssignedIds) {
String inputState;
if (theUseClientAssignedIds) {
inputState = """
GPA, AUTO, MATCH, PA0
GPA, AUTO, MATCH, PA1
GPA, AUTO, MATCH, PA2
GPB, AUTO, MATCH, PB0
""";
} else {
inputState = """
SERVER_ASSIGNED_GA, AUTO, MATCH, SERVER_ASSIGNED_PA0
SERVER_ASSIGNED_GA, AUTO, MATCH, SERVER_ASSIGNED_PA1
SERVER_ASSIGNED_GA, AUTO, MATCH, SERVER_ASSIGNED_PA2
SERVER_ASSIGNED_GB, AUTO, MATCH, SERVER_ASSIGNED_PB0
""";
}
MDMState<Patient, JpaPid> state = new MDMState<>();
state.setInputState(inputState);
MDMLinkResults outcome = myLinkHelper.setup(state);
// Group A - all have the same golden resource
createPatientOutcome = myMdmHelper.createOrUpdateWithLatch(buildPatient(theUseClientAssignedIds, "123"));
mySourcePatientA0Id = createPatientOutcome.getDaoMethodOutcome().getId().toUnqualifiedVersionless();
myGoldenResourcePatientAId = new IdType(createPatientOutcome.getMdmLinkEvent().getMdmLinks().get(0).getGoldenResourceId());
mySourcePatientA1Id = myMdmHelper.createOrUpdateWithLatch(buildPatient(theUseClientAssignedIds, "123")).getDaoMethodOutcome().getId().toUnqualifiedVersionless();
mySourcePatientA2Id = myMdmHelper.createOrUpdateWithLatch(buildPatient(theUseClientAssignedIds, "123")).getDaoMethodOutcome().getId().toUnqualifiedVersionless();
assertEquals(3, countAllMdmLinks());
mySourcePatientA0Id = toId(state, outcome.getResults().get(0).getSourcePersistenceId());
myGoldenResourcePatientAId = toId(state, outcome.getResults().get(0).getGoldenResourcePersistenceId());
mySourcePatientA1Id = toId(state, outcome.getResults().get(1).getSourcePersistenceId());
mySourcePatientA2Id = toId(state, outcome.getResults().get(2).getSourcePersistenceId());
mySourcePatientB0Id = toId(state, outcome.getResults().get(3).getSourcePersistenceId());
myGoldenResourcePatientBId = toId(state, outcome.getResults().get(3).getGoldenResourcePersistenceId());
assertEquals(4, logAllMdmLinks());
myObservationReferencingSourcePatientA0Id = createObservation(theUseClientAssignedIds, mySourcePatientA0Id, "code0");
myObservationReferencingSourcePatientA1Id = createObservation(theUseClientAssignedIds, mySourcePatientA1Id, "code1");
myObservationReferencingSourcePatientA2Id = createObservation(theUseClientAssignedIds, mySourcePatientA2Id, "code2");
myObservationReferencingGoldenPatientAId = createObservation(theUseClientAssignedIds, myGoldenResourcePatientAId, "code2");
// Group 2 - different golden resource
createPatientOutcome = myMdmHelper.createOrUpdateWithLatch(buildPatient(theUseClientAssignedIds, "456"));
mySourcePatientB0Id = createPatientOutcome.getDaoMethodOutcome().getId().toUnqualifiedVersionless();
myGoldenResourcePatientBId = new IdType(createPatientOutcome.getMdmLinkEvent().getMdmLinks().get(0).getGoldenResourceId());
assertEquals(4, logAllMdmLinks());
myObservationReferencingSourcePatientB0Id = createObservation(theUseClientAssignedIds, mySourcePatientB0Id, "code0");
assertEquals(!theUseClientAssignedIds, mySourcePatientA0Id.isIdPartValidLong());
assertEquals(!theUseClientAssignedIds, myGoldenResourcePatientAId.isIdPartValidLong());
logAllResources();
}
@Nonnull
private static IdType toId(MDMState<Patient, JpaPid> state, JpaPid persistentId) {
return new IdType("Patient/" + state.getForcedId(persistentId));
}
private IIdType createObservation(boolean theUseClientAssignedIds, IIdType patientId, String code) {
String resourceId = theUseClientAssignedIds ? UUID.randomUUID().toString() : null;
return createObservation(withIdOrNull(resourceId), withSubject(patientId), withObservationCode("http://foo", code)).toUnqualifiedVersionless();
}
private Patient buildPatient(boolean theUseClientAssignedIds, String theEid) {
String resourceId = theUseClientAssignedIds ? UUID.randomUUID().toString() : null;
Patient patient = (Patient) buildPatient(withIdOrNull(resourceId), withActiveTrue());
return addExternalEID(patient, theEid);
}
}

View File

@ -166,7 +166,7 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
Patient gp1 = state.getParameter("GP1");
Patient gp2 = state.getParameter("GP2");
Object intereptor = new Object() {
Object interceptor = new Object() {
@Hook(Pointcut.MDM_POST_MERGE_GOLDEN_RESOURCES)
void onUpdate(RequestDetails theDetails, MdmMergeEvent theEvent) {
called.getAndSet(true);
@ -175,8 +175,8 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
assertTrue(theEvent.getFromResource().isGoldenResource() && theEvent.getToResource().isGoldenResource());
}
};
myInterceptors.add(intereptor);
myInterceptorService.registerInterceptor(intereptor);
myInterceptors.add(interceptor);
myInterceptorService.registerInterceptor(interceptor);
// test
myMdmProvider.mergeGoldenResources(
@ -206,7 +206,7 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
MdmMatchResultEnum toSave = MdmMatchResultEnum.MATCH;
AtomicBoolean called = new AtomicBoolean(false);
Object intereptor = new Object() {
Object interceptor = new Object() {
@Hook(Pointcut.MDM_POST_UPDATE_LINK)
void onUpdate(RequestDetails theDetails, MdmLinkEvent theEvent) {
called.getAndSet(true);
@ -217,8 +217,8 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
assertEquals("Patient/" + gp1.getIdPart(), link.getGoldenResourceId());
}
};
myInterceptors.add(intereptor);
myInterceptorService.registerInterceptor(intereptor);
myInterceptors.add(interceptor);
myInterceptorService.registerInterceptor(interceptor);
// test
myMdmProvider.updateLink(
@ -240,7 +240,7 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
Patient golden = createGoldenPatient();
MdmMatchResultEnum match = MdmMatchResultEnum.MATCH;
Object intereptor = new Object() {
Object interceptor = new Object() {
@Hook(Pointcut.MDM_POST_CREATE_LINK)
void onCreate(RequestDetails theDetails, MdmLinkEvent theEvent) {
called.getAndSet(true);
@ -251,8 +251,8 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
assertEquals("Patient/" + golden.getIdPart(), link.getGoldenResourceId());
}
};
myInterceptors.add(intereptor);
myInterceptorService.registerInterceptor(intereptor);
myInterceptors.add(interceptor);
myInterceptorService.registerInterceptor(interceptor);
// test
myMdmProvider.createLink(

View File

@ -150,7 +150,8 @@ public interface IIdHelperService<T extends IResourcePersistentId> {
List<T> resolveResourcePersistentIdsWithCache(RequestPartitionId theRequestPartitionId, List<IIdType> theIds);
/**
* Values in the returned map are typed resource IDs (Patient/ABC)
* Value will be an empty Optional if the PID doesn't exist, or
* a typed resource ID if so (Patient/ABC).
*/
Optional<String> translatePidIdToForcedIdWithCache(T theResourcePersistentId);