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