Merge branch 'master' into 6180-fix-client
This commit is contained in:
commit
a668a1b08b
|
@ -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 =
|
||||
|
|
|
@ -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."
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue