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}