001/*- 002 * #%L 003 * HAPI FHIR - Master Data Management 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.mdm.provider; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 026import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; 027import ca.uhn.fhir.mdm.api.IMdmSettings; 028import ca.uhn.fhir.mdm.api.MatchedTarget; 029import ca.uhn.fhir.mdm.api.MdmConstants; 030import ca.uhn.fhir.mdm.util.MdmResourceUtil; 031import ca.uhn.fhir.mdm.util.MessageHelper; 032import ca.uhn.fhir.model.primitive.IdDt; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 035import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 036import ca.uhn.fhir.rest.server.provider.ProviderConstants; 037import ca.uhn.fhir.util.BundleBuilder; 038import ca.uhn.fhir.validation.IResourceLoader; 039import jakarta.annotation.Nonnull; 040import org.hl7.fhir.instance.model.api.IAnyResource; 041import org.hl7.fhir.instance.model.api.IBase; 042import org.hl7.fhir.instance.model.api.IBaseBackboneElement; 043import org.hl7.fhir.instance.model.api.IBaseBundle; 044import org.hl7.fhir.instance.model.api.IBaseDatatype; 045import org.hl7.fhir.instance.model.api.IBaseExtension; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047import org.hl7.fhir.instance.model.api.IIdType; 048import org.springframework.beans.factory.annotation.Autowired; 049import org.springframework.stereotype.Service; 050 051import java.math.BigDecimal; 052import java.util.Comparator; 053import java.util.Date; 054import java.util.List; 055import java.util.UUID; 056 057@Service 058public class MdmControllerHelper { 059 060 private final FhirContext myFhirContext; 061 private final IResourceLoader myResourceLoader; 062 private final IMdmSettings myMdmSettings; 063 private final MessageHelper myMessageHelper; 064 private final IMdmMatchFinderSvc myMdmMatchFinderSvc; 065 private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 066 067 @Autowired 068 public MdmControllerHelper( 069 FhirContext theFhirContext, 070 IResourceLoader theResourceLoader, 071 IMdmMatchFinderSvc theMdmMatchFinderSvc, 072 IMdmSettings theMdmSettings, 073 MessageHelper theMessageHelper, 074 IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { 075 myFhirContext = theFhirContext; 076 myResourceLoader = theResourceLoader; 077 myMdmSettings = theMdmSettings; 078 myMdmMatchFinderSvc = theMdmMatchFinderSvc; 079 myMessageHelper = theMessageHelper; 080 myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; 081 } 082 083 public void validateSameVersion(IAnyResource theResource, String theResourceId) { 084 String storedId = theResource.getIdElement().getValue(); 085 if (hasVersionIdPart(theResourceId) && !storedId.equals(theResourceId)) { 086 throw new ResourceVersionConflictException(Msg.code(1501) + "Requested resource " + theResourceId 087 + " is not the latest version. Latest version is " + storedId); 088 } 089 } 090 091 private boolean hasVersionIdPart(String theId) { 092 return new IdDt(theId).hasVersionIdPart(); 093 } 094 095 public IAnyResource getLatestGoldenResourceFromIdOrThrowException(String theParamName, String theGoldenResourceId) { 096 IdDt resourceId = MdmControllerUtil.getGoldenIdDtOrThrowException(theParamName, theGoldenResourceId); 097 IAnyResource iAnyResource = loadResource(resourceId.toUnqualifiedVersionless()); 098 if (MdmResourceUtil.isGoldenRecord(iAnyResource)) { 099 return iAnyResource; 100 } else { 101 throw new InvalidRequestException(Msg.code(1502) 102 + myMessageHelper.getMessageForFailedGoldenResourceLoad(theParamName, theGoldenResourceId)); 103 } 104 } 105 106 public IAnyResource getLatestSourceFromIdOrThrowException(String theParamName, String theSourceId) { 107 IIdType sourceId = MdmControllerUtil.getSourceIdDtOrThrowException(theParamName, theSourceId); 108 return loadResource(sourceId.toUnqualifiedVersionless()); 109 } 110 111 protected IAnyResource loadResource(IIdType theResourceId) { 112 Class<? extends IBaseResource> resourceClass = myFhirContext 113 .getResourceDefinition(theResourceId.getResourceType()) 114 .getImplementingClass(); 115 return (IAnyResource) myResourceLoader.load(resourceClass, theResourceId); 116 } 117 118 public void validateMergeResources(IAnyResource theFromGoldenResource, IAnyResource theToGoldenResource) { 119 validateIsMdmManaged(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResource); 120 validateIsMdmManaged(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResource); 121 } 122 123 public String toJson(IAnyResource theAnyResource) { 124 return myFhirContext.newJsonParser().encodeResourceToString(theAnyResource); 125 } 126 127 public void validateIsMdmManaged(String theName, IAnyResource theResource) { 128 String resourceType = myFhirContext.getResourceType(theResource); 129 if (!myMdmSettings.isSupportedMdmType(resourceType)) { 130 throw new InvalidRequestException( 131 Msg.code(1503) + myMessageHelper.getMessageForUnsupportedResource(theName, resourceType)); 132 } 133 134 if (!MdmResourceUtil.isMdmManaged(theResource)) { 135 throw new InvalidRequestException(Msg.code(1504) + myMessageHelper.getMessageForUnmanagedResource()); 136 } 137 } 138 139 /** 140 * Helper method which will return a bundle of all Matches and Possible Matches. 141 */ 142 public IBaseBundle getMatchesAndPossibleMatchesForResource( 143 IAnyResource theResource, String theResourceType, RequestDetails theRequestDetails) { 144 RequestPartitionId requestPartitionId; 145 if (myMdmSettings.getSearchAllPartitionForMatch()) { 146 requestPartitionId = RequestPartitionId.allPartitions(); 147 } else { 148 requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( 149 theRequestDetails, theResourceType); 150 } 151 List<MatchedTarget> matches = 152 myMdmMatchFinderSvc.getMatchedTargets(theResourceType, theResource, requestPartitionId); 153 matches.sort( 154 Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()) 155 .reversed()); 156 157 BundleBuilder builder = new BundleBuilder(myFhirContext); 158 builder.setBundleField("type", "searchset"); 159 builder.setBundleField("id", UUID.randomUUID().toString()); 160 builder.setMetaField("lastUpdated", builder.newPrimitive("instant", new Date())); 161 162 IBaseBundle retVal = builder.getBundle(); 163 for (MatchedTarget next : matches) { 164 boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); 165 if (!shouldKeepThisEntry) { 166 continue; 167 } 168 169 IBase entry = builder.addEntry(); 170 builder.addToEntry(entry, "resource", next.getTarget()); 171 172 IBaseBackboneElement search = builder.addSearch(entry); 173 toBundleEntrySearchComponent(builder, search, next); 174 } 175 return retVal; 176 } 177 178 public IBaseBackboneElement toBundleEntrySearchComponent( 179 BundleBuilder theBuilder, IBaseBackboneElement theSearch, MatchedTarget theMatchedTarget) { 180 theBuilder.setSearchField(theSearch, "mode", "match"); 181 double score = theMatchedTarget.getMatchResult().getNormalizedScore(); 182 theBuilder.setSearchField(theSearch, "score", theBuilder.newPrimitive("decimal", BigDecimal.valueOf(score))); 183 184 String matchGrade = getMatchGrade(theMatchedTarget); 185 IBaseDatatype codeType = 186 (IBaseDatatype) myFhirContext.getElementDefinition("code").newInstance(matchGrade); 187 IBaseExtension searchExtension = theSearch.addExtension(); 188 searchExtension.setUrl(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE); 189 searchExtension.setValue(codeType); 190 191 return theSearch; 192 } 193 194 @Nonnull 195 protected String getMatchGrade(MatchedTarget theTheMatchedTarget) { 196 String retVal = "probable"; 197 if (theTheMatchedTarget.isMatch()) { 198 retVal = "certain"; 199 } else if (theTheMatchedTarget.isPossibleMatch()) { 200 retVal = "possible"; 201 } 202 return retVal; 203 } 204}