Merge branch 'master' into 6180-fix-client

This commit is contained in:
leif stawnyczy 2024-08-09 13:17:42 -04:00
commit a668a1b08b
18 changed files with 266 additions and 35 deletions

View File

@ -209,7 +209,7 @@ public class ParametersUtil {
*
* @param theContext The FhirContext
* @param theParameters The Parameters resource
* @param theName The parametr name
* @param theName The parameter name
* @param theValue The parameter value (can be a {@link IBaseResource resource} or a {@link IBaseDatatype datatype})
*/
public static void addParameterToParameters(
@ -248,7 +248,7 @@ public class ParametersUtil {
private static IBase createParameterRepetition(
FhirContext theContext,
IBaseResource theTargetResource,
IBase theTargetResource,
BaseRuntimeChildDefinition paramChild,
BaseRuntimeElementCompositeDefinition<?> paramChildElem,
String theName) {
@ -458,6 +458,17 @@ public class ParametersUtil {
return part;
}
public static IBase createPart(FhirContext theContext, IBase thePart, String theName) {
BaseRuntimeElementCompositeDefinition<?> def =
(BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(thePart.getClass());
BaseRuntimeChildDefinition partChild = def.getChildByName("part");
BaseRuntimeElementCompositeDefinition<?> partChildElem =
(BaseRuntimeElementCompositeDefinition<?>) partChild.getChildByName("part");
return createParameterRepetition(theContext, thePart, partChild, partChildElem, theName);
}
public static void addPartResource(
FhirContext theContext, IBase theParameter, String theName, IBaseResource theValue) {
BaseRuntimeElementCompositeDefinition<?> def =

View File

@ -0,0 +1,5 @@
---
type: add
issue: 5138
title: "A match result map field has been added to the `$mdm-link-history` operation. This new field shows the rules that
evaluated true during matching and the corresponding initial match result when the link was created."

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.mdm.svc;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.mdm.api.IMdmLink;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.api.MdmLinkWithRevision;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkWithRevisionJson;
@ -35,6 +36,9 @@ public class MdmModelConverterSvcImpl implements IMdmModelConverterSvc {
@Autowired
IIdHelperService myIdHelperService;
@Autowired
private IMdmSettings myMdmSettings;
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public MdmLinkJson toJson(IMdmLink theLink) {
@ -69,6 +73,7 @@ public class MdmModelConverterSvcImpl implements IMdmModelConverterSvc {
retVal.setUpdated(theLink.getUpdated());
retVal.setVersion(theLink.getVersion());
retVal.setRuleCount(theLink.getRuleCount());
retVal.translateAndSetRule(myMdmSettings.getMdmRules(), theLink.getVector());
return retVal;
}

View File

@ -642,7 +642,7 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
return myMdmLinkDao.save(mdmLink);
}
protected MdmLink createGoldenPatientAndLinkToSourcePatient(MdmMatchResultEnum theMdmMatchResultEnum, MdmLinkSourceEnum theMdmLinkSourceEnum, String theVersion, Date theCreateTime, Date theUpdateTime, boolean theLinkCreatedNewResource) {
protected MdmLink createGoldenPatientAndLinkToSourcePatient(MdmMatchResultEnum theMdmMatchResultEnum, MdmLinkSourceEnum theMdmLinkSourceEnum, String theVersion, Date theCreateTime, Date theUpdateTime, boolean theLinkCreatedNewResource, Long theVector) {
final Patient goldenPatient = createPatient();
final Patient sourcePatient = createPatient();
@ -655,6 +655,7 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
mdmLink.setGoldenResourcePersistenceId(runInTransaction(()->myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), goldenPatient)));
mdmLink.setSourcePersistenceId(runInTransaction(()->myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), sourcePatient)));
mdmLink.setHadToCreateNewGoldenResource(theLinkCreatedNewResource);
mdmLink.setVector(theVector);
return myMdmLinkDao.save(mdmLink);
}

View File

@ -38,19 +38,20 @@ public class MdmModelConverterSvcImplTest extends BaseMdmR4Test {
final Date updateTime = new Date();
final String version = "1";
final boolean isLinkCreatedResource = false;
final long vector = 4L;
final double score = 0.8333333333333;
final double scoreRounded = BigDecimal.valueOf(score).setScale(4, RoundingMode.HALF_UP).doubleValue();
MdmLink mdmLink = createGoldenPatientAndLinkToSourcePatient(MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource);
final MdmLink mdmLink = createGoldenPatientAndLinkToSourcePatient(MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, vector);
mdmLink.setScore(score);
mdmLink.setVector(61L);
myMdmLinkDao.save(mdmLink);
final MdmLinkJson actualMdmLinkJson = myMdmModelConverterSvc.toJson(mdmLink);
ourLog.info("actualMdmLinkJson: {}", actualMdmLinkJson);
MdmLinkJson expectedMdmLinkJson = getExepctedMdmLinkJson(mdmLink.getGoldenResourcePersistenceId().getId(), mdmLink.getSourcePersistenceId().getId(), MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, scoreRounded);
MdmLinkJson expectedMdmLinkJson = getExepctedMdmLinkJson(mdmLink.getGoldenResourcePersistenceId().getId(), mdmLink.getSourcePersistenceId().getId(), MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, vector, scoreRounded);
assertEquals(expectedMdmLinkJson.getSourceId(), actualMdmLinkJson.getSourceId());
assertEquals(expectedMdmLinkJson.getGoldenResourceId(), actualMdmLinkJson.getGoldenResourceId());
assertEquals(expectedMdmLinkJson.getGoldenPid().getId(), actualMdmLinkJson.getGoldenPid().getId());
@ -59,6 +60,7 @@ public class MdmModelConverterSvcImplTest extends BaseMdmR4Test {
assertEquals(expectedMdmLinkJson.getScore(), actualMdmLinkJson.getScore());
assertEquals(expectedMdmLinkJson.getMatchResult(), actualMdmLinkJson.getMatchResult());
assertEquals(expectedMdmLinkJson.getLinkSource(), actualMdmLinkJson.getLinkSource());
assertEquals(getExepctedMdmLinkJson(mdmLink.getGoldenResourcePersistenceId().getId(), mdmLink.getSourcePersistenceId().getId(), MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, vector, scoreRounded), actualMdmLinkJson);
}
@Test
@ -72,12 +74,12 @@ public class MdmModelConverterSvcImplTest extends BaseMdmR4Test {
final String version = "1";
final boolean isLinkCreatedResource = false;
final long revisionNumber = 2L;
final long vector = 4L;
final double score = 0.8333333333333;
final double scoreRounded = BigDecimal.valueOf(score).setScale(4, RoundingMode.HALF_UP).doubleValue();
MdmLink mdmLink = createGoldenPatientAndLinkToSourcePatient(MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource);
final MdmLink mdmLink = createGoldenPatientAndLinkToSourcePatient(MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, vector);
mdmLink.setScore(score);
mdmLink.setVector(61L);
myMdmLinkDao.save(mdmLink);
final MdmLinkWithRevision<IMdmLink<? extends IResourcePersistentId<?>>> revision = new MdmLinkWithRevision<>(mdmLink, new EnversRevision(RevisionType.ADD, revisionNumber, revisionTimestamp));
@ -85,7 +87,7 @@ public class MdmModelConverterSvcImplTest extends BaseMdmR4Test {
final MdmLinkWithRevisionJson actualMdmLinkWithRevisionJson = myMdmModelConverterSvc.toJson(revision);
final MdmLinkWithRevisionJson expectedMdmLinkWithRevisionJson =
new MdmLinkWithRevisionJson(getExepctedMdmLinkJson(mdmLink.getGoldenResourcePersistenceId().getId(), mdmLink.getSourcePersistenceId().getId(), MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, scoreRounded), revisionNumber, revisionTimestamp);
new MdmLinkWithRevisionJson(getExepctedMdmLinkJson(mdmLink.getGoldenResourcePersistenceId().getId(), mdmLink.getSourcePersistenceId().getId(), MdmMatchResultEnum.MATCH, MdmLinkSourceEnum.MANUAL, version, createTime, updateTime, isLinkCreatedResource, vector, scoreRounded), revisionNumber, revisionTimestamp);
assertMdmLinkRevisionsEqual(expectedMdmLinkWithRevisionJson, actualMdmLinkWithRevisionJson);
}
@ -104,7 +106,7 @@ public class MdmModelConverterSvcImplTest extends BaseMdmR4Test {
assertEquals(theExpectedMdmLinkWithRevisionJson.getRevisionTimestamp(), theActualMdmLinkWithRevisionJson.getRevisionTimestamp());
}
private MdmLinkJson getExepctedMdmLinkJson(Long theGoldenPatientId, Long theSourceId, MdmMatchResultEnum theMdmMatchResultEnum, MdmLinkSourceEnum theMdmLinkSourceEnum, String version, Date theCreateTime, Date theUpdateTime, boolean theLinkCreatedNewResource, double theScore) {
private MdmLinkJson getExepctedMdmLinkJson(Long theGoldenPatientId, Long theSourceId, MdmMatchResultEnum theMdmMatchResultEnum, MdmLinkSourceEnum theMdmLinkSourceEnum, String version, Date theCreateTime, Date theUpdateTime, boolean theLinkCreatedNewResource, Long theVector, double theScore) {
final MdmLinkJson mdmLinkJson = new MdmLinkJson();
mdmLinkJson.setGoldenResourceId("Patient/" + theGoldenPatientId);
@ -121,6 +123,7 @@ public class MdmModelConverterSvcImplTest extends BaseMdmR4Test {
// make sure vector is not converted
mdmLinkJson.setVector(null);
mdmLinkJson.translateAndSetRule(myMdmSettings.getMdmRules(), theVector);
return mdmLinkJson;
}

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.XmlUtil;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
@ -24,6 +25,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@Disabled
public class FhirPatchCoreTest extends BaseTest {
private static final Logger ourLog = LoggerFactory.getLogger(FhirPatchCoreTest.class);
@ -50,9 +52,12 @@ public class FhirPatchCoreTest extends BaseTest {
public static List<Object[]> parameters() throws TransformerException, SAXException, IOException {
String testSpecR4 = "/org/hl7/fhir/testcases/r4/patch/fhir-path-tests.xml";
// the above file is missing an <output></output> section on line 1413 due to a processing error during file generation
// as per the error statement found in the file.
Collection<Object[]> retValR4 = loadTestSpec(FhirContext.forR4Cached(), testSpecR4);
String testSpecR5 = "/org/hl7/fhir/testcases/r5/patch/fhir-path-tests.xml";
// The above file his missing xml closing tag '/>' on line 241 and 245
Collection<Object[]> retValR5 = loadTestSpec(FhirContext.forR5Cached(), testSpecR5);
ArrayList<Object[]> retVal = new ArrayList<>();

View File

@ -90,4 +90,17 @@ public class MdmHistorySearchParameters {
return MdmControllerUtil.extractGoldenResourceIdDtOrNull(
ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theTheGoldenResourceId);
}
public enum SearchOperatorEnum {
/**
* Used to indicate we should perform an OR search between all IDs provided
* ie. links only need at least 1 of the IDs provided in the search parameters
*/
OR,
/**
* Used to indicate we should perform an AND search between all IDs provided
* ie. links must contain all IDs provided in the search parameters
*/
AND
}
}

View File

@ -21,13 +21,17 @@ package ca.uhn.fhir.mdm.model.mdmevents;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class MdmLinkJson implements IModelJson {
@ -97,6 +101,14 @@ public class MdmLinkJson implements IModelJson {
@JsonProperty("ruleCount")
private Long myRuleCount;
/**
* Matched rules are the rules, as defined in the matchResultMap, that evaluated true
* for this link. This property stores the name of the rule, represented by a String,
* and the corresponding MatchResultEnum for that rule.
*/
@JsonProperty(value = "matchedRules")
private Set<Map.Entry<String, MdmMatchResultEnum>> myRule = new HashSet<>();
public String getGoldenResourceId() {
return myGoldenResourceId;
}
@ -204,6 +216,14 @@ public class MdmLinkJson implements IModelJson {
myRuleCount = theRuleCount;
}
public Set<Map.Entry<String, MdmMatchResultEnum>> getRule() {
return myRule;
}
public void translateAndSetRule(MdmRulesJson theRule, Long theVector) {
myRule = theRule.getMatchedRulesFromVectorMap(theVector);
}
public IResourcePersistentId<?> getGoldenPid() {
return myGoldenPid;
}

View File

@ -21,10 +21,12 @@ package ca.uhn.fhir.mdm.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.paging.MdmPageLinkBuilder;
import ca.uhn.fhir.mdm.api.paging.MdmPageLinkTuple;
import ca.uhn.fhir.mdm.api.paging.MdmPageRequest;
import ca.uhn.fhir.mdm.api.params.MdmHistorySearchParameters;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkWithRevisionJson;
@ -44,16 +46,21 @@ import org.springframework.data.domain.Page;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public abstract class BaseMdmProvider {
protected final FhirContext myFhirContext;
protected final IMdmControllerSvc myMdmControllerSvc;
public BaseMdmProvider(FhirContext theFhirContext) {
public BaseMdmProvider(FhirContext theFhirContext, IMdmControllerSvc theMdmControllerSvc) {
myFhirContext = theFhirContext;
myMdmControllerSvc = theMdmControllerSvc;
}
protected void validateMergeParameters(
@ -214,7 +221,9 @@ public abstract class BaseMdmProvider {
}
protected void parametersFromMdmLinkRevisions(
IBaseParameters theRetVal, List<MdmLinkWithRevisionJson> theMdmLinkRevisions) {
IBaseParameters theRetVal,
List<MdmLinkWithRevisionJson> theMdmLinkRevisions,
ServletRequestDetails theRequestDetails) {
if (theMdmLinkRevisions.isEmpty()) {
final IBase resultPart = ParametersUtil.addParameterToParameters(
myFhirContext, theRetVal, "historical links not found for query parameters");
@ -223,26 +232,72 @@ public abstract class BaseMdmProvider {
myFhirContext, resultPart, "theResults", "historical links not found for query parameters");
}
theMdmLinkRevisions.forEach(mdmLinkRevision -> parametersFromMdmLinkRevision(theRetVal, mdmLinkRevision));
theMdmLinkRevisions.forEach(mdmLinkRevision -> parametersFromMdmLinkRevision(
theRetVal,
mdmLinkRevision,
findInitialMatchResult(theMdmLinkRevisions, mdmLinkRevision, theRequestDetails)));
}
private void parametersFromMdmLinkRevision(IBaseParameters retVal, MdmLinkWithRevisionJson mdmLinkRevision) {
final IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retVal, "historical link");
final MdmLinkJson mdmLink = mdmLinkRevision.getMdmLink();
private MdmMatchResultEnum findInitialMatchResult(
List<MdmLinkWithRevisionJson> theRevisionList,
MdmLinkWithRevisionJson theToMatch,
ServletRequestDetails theRequestDetails) {
String sourceId = theToMatch.getMdmLink().getSourceId();
String goldenId = theToMatch.getMdmLink().getGoldenResourceId();
// In the REDIRECT case, both the goldenResourceId and sourceResourceId fields are actually both
// golden resources. Because of this, based on our history query, it's possible not all links
// involving that golden resource will show up in the results (eg. query for goldenResourceId = GR/1
// but sourceResourceId = GR/1 in the link history). Hence, we should re-query to find the initial
// match result.
if (theToMatch.getMdmLink().getMatchResult() == MdmMatchResultEnum.REDIRECT) {
MdmHistorySearchParameters params = new MdmHistorySearchParameters()
.setSourceIds(List.of(sourceId, goldenId))
.setGoldenResourceIds(List.of(sourceId, goldenId));
List<MdmLinkWithRevisionJson> result = myMdmControllerSvc.queryLinkHistory(params, theRequestDetails);
// If there is a POSSIBLE_DUPLICATE, a user merged two resources with *pre-existing* POSSIBLE_DUPLICATE link
// so the initial match result is POSSIBLE_DUPLICATE
// If no POSSIBLE_DUPLICATE, a user merged two *unlinked* GRs, so the initial match result is REDIRECT
return containsPossibleDuplicate(result)
? MdmMatchResultEnum.POSSIBLE_DUPLICATE
: MdmMatchResultEnum.REDIRECT;
}
// Get first match result with given source and golden ID
Optional<MdmLinkWithRevisionJson> theEarliestRevision = theRevisionList.stream()
.filter(revision -> revision.getMdmLink().getSourceId().equals(sourceId))
.filter(revision -> revision.getMdmLink().getGoldenResourceId().equals(goldenId))
.min(Comparator.comparing(MdmLinkWithRevisionJson::getRevisionNumber));
return theEarliestRevision.isPresent()
? theEarliestRevision.get().getMdmLink().getMatchResult()
: theToMatch.getMdmLink().getMatchResult();
}
private static boolean containsPossibleDuplicate(List<MdmLinkWithRevisionJson> result) {
return result.stream().anyMatch(t -> t.getMdmLink().getMatchResult() == MdmMatchResultEnum.POSSIBLE_DUPLICATE);
}
private void parametersFromMdmLinkRevision(
IBaseParameters theRetVal,
MdmLinkWithRevisionJson theMdmLinkRevision,
MdmMatchResultEnum theInitialAutoResult) {
final IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, theRetVal, "historical link");
final MdmLinkJson mdmLink = theMdmLinkRevision.getMdmLink();
ParametersUtil.addPartString(myFhirContext, resultPart, "goldenResourceId", mdmLink.getGoldenResourceId());
ParametersUtil.addPartString(
myFhirContext,
resultPart,
"revisionTimestamp",
mdmLinkRevision.getRevisionTimestamp().toString());
theMdmLinkRevision.getRevisionTimestamp().toString());
ParametersUtil.addPartString(myFhirContext, resultPart, "sourceResourceId", mdmLink.getSourceId());
ParametersUtil.addPartString(
myFhirContext,
resultPart,
"matchResult",
mdmLink.getMatchResult().name());
ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", mdmLink.getScore());
ParametersUtil.addPartString(
myFhirContext, resultPart, "linkSource", mdmLink.getLinkSource().name());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", mdmLink.getEidMatch());
@ -253,6 +308,16 @@ public abstract class BaseMdmProvider {
mdmLink.getCreated().getTime());
ParametersUtil.addPartDecimal(myFhirContext, resultPart, "linkUpdated", (double)
mdmLink.getUpdated().getTime());
IBase matchResultMapSubpart = ParametersUtil.createPart(myFhirContext, resultPart, "matchResultMap");
IBase matchedRulesSubpart = ParametersUtil.createPart(myFhirContext, matchResultMapSubpart, "matchedRules");
for (Map.Entry<String, MdmMatchResultEnum> entry : mdmLink.getRule()) {
ParametersUtil.addPartString(myFhirContext, matchedRulesSubpart, "rule", entry.getKey());
}
ParametersUtil.addPartString(
myFhirContext, matchResultMapSubpart, "initialMatchResult", theInitialAutoResult.name());
}
protected void addPagingParameters(

View File

@ -48,16 +48,13 @@ import static org.slf4j.LoggerFactory.getLogger;
public class MdmLinkHistoryProviderDstu3Plus extends BaseMdmProvider {
private static final Logger ourLog = getLogger(MdmLinkHistoryProviderDstu3Plus.class);
private final IMdmControllerSvc myMdmControllerSvc;
private final IInterceptorBroadcaster myInterceptorBroadcaster;
public MdmLinkHistoryProviderDstu3Plus(
FhirContext theFhirContext,
IMdmControllerSvc theMdmControllerSvc,
IInterceptorBroadcaster theIInterceptorBroadcaster) {
super(theFhirContext);
myMdmControllerSvc = theMdmControllerSvc;
super(theFhirContext, theMdmControllerSvc);
myInterceptorBroadcaster = theIInterceptorBroadcaster;
}
@ -93,7 +90,7 @@ public class MdmLinkHistoryProviderDstu3Plus extends BaseMdmProvider {
final List<MdmLinkWithRevisionJson> mdmLinkRevisionsFromSvc =
myMdmControllerSvc.queryLinkHistory(mdmHistorySearchParameters, theRequestDetails);
parametersFromMdmLinkRevisions(retVal, mdmLinkRevisionsFromSvc);
parametersFromMdmLinkRevisions(retVal, mdmLinkRevisionsFromSvc, theRequestDetails);
if (myInterceptorBroadcaster.hasHooks(Pointcut.MDM_POST_LINK_HISTORY)) {
// MDM_POST_LINK_HISTORY hook

View File

@ -74,7 +74,6 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider {
private static final String PATIENT_RESOURCE = "Patient";
private static final String PRACTITIONER_RESOURCE = "Practitioner";
private final IMdmControllerSvc myMdmControllerSvc;
private final IMdmSubmitSvc myMdmSubmitSvc;
private final IMdmSettings myMdmSettings;
private final MdmControllerHelper myMdmControllerHelper;
@ -97,8 +96,7 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider {
IMdmSubmitSvc theMdmSubmitSvc,
IInterceptorBroadcaster theIInterceptorBroadcaster,
IMdmSettings theIMdmSettings) {
super(theFhirContext);
myMdmControllerSvc = theMdmControllerSvc;
super(theFhirContext, theMdmControllerSvc);
myMdmControllerHelper = theMdmHelper;
myMdmSubmitSvc = theMdmSubmitSvc;
myInterceptorBroadcaster = theIInterceptorBroadcaster;

View File

@ -33,6 +33,8 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
@ -207,6 +209,13 @@ public class MdmRulesJson implements IModelJson {
return myVectorMatchResultMap.getFieldMatchNames(theVector);
}
public Set<Map.Entry<String, MdmMatchResultEnum>> getMatchedRulesFromVectorMap(Long theLong) {
Set<String> matchedRules = myVectorMatchResultMap.getMatchedRules(theLong);
return myMatchResultMap.entrySet().stream()
.filter(e -> matchedRules.contains(e.getKey()))
.collect(Collectors.toSet());
}
public String getDetailedFieldMatchResultWithSuccessInformation(long theVector) {
List<String> fieldMatchResult = new ArrayList<>();
for (int i = 0; i < myMatchFieldJsonList.size(); ++i) {

View File

@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class VectorMatchResultMap {
private final MdmRulesJson myMdmRulesJson;
@ -64,6 +65,16 @@ public class VectorMatchResultMap {
return MdmMatchResultEnum.NO_MATCH;
}
public Set<String> getMatchedRules(Long theVector) {
if (theVector == null) {
return new HashSet<>();
}
return myVectorToFieldMatchNamesMap.entrySet().stream()
.filter(e -> ((e.getKey() & theVector) == e.getKey()))
.map(Map.Entry::getValue)
.collect(Collectors.toSet());
}
private void put(String theFieldMatchNames, MdmMatchResultEnum theMatchResult) {
long vector = getVector(theFieldMatchNames);
myVectorToFieldMatchNamesMap.put(vector, theFieldMatchNames);
@ -104,4 +115,10 @@ public class VectorMatchResultMap {
public String getFieldMatchNames(long theVector) {
return myVectorToFieldMatchNamesMap.get(theVector);
}
public Set<String> getAllFieldMatchNames() {
return myVectorToFieldMatchNamesMap.keySet().stream()
.map(key -> myVectorToFieldMatchNamesMap.get(key))
.collect(Collectors.toSet());
}
}

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.rest.api.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
@ -30,6 +31,7 @@ import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -39,6 +41,7 @@ import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -50,6 +53,8 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public abstract class RequestDetails {
public static final byte[] BAD_STREAM_PLACEHOLDER =
(Msg.code(2543) + "PLACEHOLDER WHEN READING FROM BAD STREAM").getBytes(StandardCharsets.UTF_8);
private final StopWatch myRequestStopwatch;
private IInterceptorBroadcaster myInterceptorBroadcaster;
private String myTenantId;
@ -523,9 +528,22 @@ public abstract class RequestDetails {
mySubRequest = theSubRequest;
}
public final byte[] loadRequestContents() {
public final synchronized byte[] loadRequestContents() {
if (myRequestContents == null) {
myRequestContents = getByteStreamRequestContents();
// Initialize the byte array to a non-null value to avoid repeated calls to getByteStreamRequestContents()
// which can occur when getByteStreamRequestContents() throws an Exception
myRequestContents = ArrayUtils.EMPTY_BYTE_ARRAY;
try {
myRequestContents = getByteStreamRequestContents();
} finally {
if (myRequestContents == null) {
// if reading the stream throws an exception, then our contents are still null, but the stream is
// dead.
// Set a placeholder value so nobody tries to read again.
myRequestContents = BAD_STREAM_PLACEHOLDER;
}
}
assert myRequestContents != null : "We must not re-read the stream.";
}
return getRequestContentsIfLoaded();
}

View File

@ -1038,6 +1038,9 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
theRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, getServletContext());
// keep track of any unhandled exceptions in case the exception handler throws another exception
Throwable unhandledException = null;
try {
/* ***********************************
@ -1205,6 +1208,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
} catch (NotModifiedException | AuthenticationException e) {
unhandledException = e;
HookParams handleExceptionParams = new HookParams();
handleExceptionParams.add(RequestDetails.class, requestDetails);
handleExceptionParams.add(ServletRequestDetails.class, requestDetails);
@ -1216,9 +1221,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
writeExceptionToResponse(theResponse, e);
unhandledException = null;
} catch (Throwable e) {
unhandledException = e;
/*
* We have caught an exception during request processing. This might be because a handling method threw
* something they wanted to throw (e.g. UnprocessableEntityException because the request
@ -1285,9 +1293,18 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
* If nobody handles it, default behaviour is to stream back the OperationOutcome to the client.
*/
DEFAULT_EXCEPTION_HANDLER.handleException(requestDetails, exception, theRequest, theResponse);
unhandledException = null;
} finally {
if (unhandledException != null) {
ourLog.error(
Msg.code(2544) + "Exception handling threw an exception. Initial exception was: {}",
unhandledException.getMessage(),
unhandledException);
unhandledException = null;
}
HookParams params = new HookParams();
params.add(RequestDetails.class, requestDetails);
params.addIfMatchesType(ServletRequestDetails.class, requestDetails);

View File

@ -1,16 +1,22 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.test.util.LogbackTestExtension;
import ca.uhn.test.util.LogbackTestExtensionAssert;
import com.helger.commons.collection.iterate.EmptyEnumeration;
import com.helger.commons.io.stream.StringInputStream;
import jakarta.annotation.Nonnull;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -36,10 +42,12 @@ import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
@ -52,6 +60,7 @@ public class ServerConcurrencyTest {
@RegisterExtension
private static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerProvider(new MyPatientProvider());
public static final String SEARCH_TIMEOUT_ERROR = "SEARCH_TIMEOUT_ERROR: Search timed out";
@RegisterExtension
private final HttpClientExtension myHttpClient = new HttpClientExtension();
@ -62,10 +71,12 @@ public class ServerConcurrencyTest {
@Mock
private PrintWriter myWriter;
private HashMap<String, String> myHeaders;
@RegisterExtension
LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension();
@Test
public void testExceptionClosingInputStream() throws IOException {
initRequestMocks();
initRequestMocks("/Patient");
DelegatingServletInputStream inputStream = createMockPatientBodyServletInputStream();
inputStream.setExceptionOnClose(true);
when(myRequest.getInputStream()).thenReturn(inputStream);
@ -78,7 +89,7 @@ public class ServerConcurrencyTest {
@Test
public void testExceptionClosingOutputStream() throws IOException {
initRequestMocks();
initRequestMocks("/Patient");
when(myRequest.getInputStream()).thenReturn(createMockPatientBodyServletInputStream());
when(myResponse.getWriter()).thenReturn(myWriter);
@ -90,12 +101,44 @@ public class ServerConcurrencyTest {
);
}
private void initRequestMocks() {
/**
* Exception thrown during SERVER_OUTGOING_FAILURE_OPERATIONOUTCOME
*/
@Test
void testExceptionThrownDuringExceptionHandler_bothExceptionsLogged() throws ServletException, IOException {
// given
initRequestMocks("/Patient?active=true");
ourServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.SERVER_OUTGOING_FAILURE_OPERATIONOUTCOME, (pointcut,params)->{
throw new RuntimeException("MARKER_2: Exception during exception processing");
});
// when
try {
ourServer.getRestfulServer().handleRequest(RequestTypeEnum.GET, myRequest, myResponse);
} catch (Throwable e) {
// eat any ex that escapes. We need to not depend on default logging.
}
// then
// both exceptions should be logged.
LogbackTestExtensionAssert.assertThat(myLogbackTestExtension).hasErrorMessage("HAPI-2544");
LogbackTestExtensionAssert.assertThat(myLogbackTestExtension).hasErrorMessage(SEARCH_TIMEOUT_ERROR);
}
private void initRequestMocks(String theURL) {
myHeaders = new HashMap<>();
myHeaders.put(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW);
when(myRequest.getRequestURI()).thenReturn("/Patient");
when(myRequest.getRequestURL()).thenReturn(new StringBuffer(ourServer.getBaseUrl() + "/Patient"));
String relativeUri;
int endOfUri = theURL.indexOf("?");
if (endOfUri >= 0) {
relativeUri = theURL.substring(0,endOfUri);
} else {
relativeUri = theURL;
}
when(myRequest.getRequestURI()).thenReturn(relativeUri);
when(myRequest.getRequestURL()).thenReturn(new StringBuffer(ourServer.getBaseUrl() + theURL));
when(myRequest.getHeader(any())).thenAnswer(t -> {
String header = t.getArgument(0, String.class);
String value = myHeaders.get(header);
@ -190,6 +233,11 @@ public class ServerConcurrencyTest {
.setOperationOutcome(oo);
}
@Search
public List<Patient> search() throws InterruptedException {
throw new InterruptedException(SEARCH_TIMEOUT_ERROR);
}
@Override
public Class<Patient> getResourceType() {

View File

@ -312,7 +312,6 @@
<dependency>
<groupId>org.hl7.fhir.testcases</groupId>
<artifactId>fhir-test-cases</artifactId>
<version>1.5.5-SNAPSHOT</version>
<scope>test</scope>
</dependency>

View File

@ -1306,7 +1306,7 @@
<dependency>
<groupId>org.hl7.fhir.testcases</groupId>
<artifactId>fhir-test-cases</artifactId>
<version>1.1.14</version>
<version>1.5.15</version>
</dependency>
<dependency>
<groupId>org.jdom</groupId>