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.mdm.api.MdmMatchResultEnum;
025import ca.uhn.fhir.mdm.api.paging.MdmPageLinkBuilder;
026import ca.uhn.fhir.mdm.api.paging.MdmPageLinkTuple;
027import ca.uhn.fhir.mdm.api.paging.MdmPageRequest;
028import ca.uhn.fhir.mdm.model.MdmTransactionContext;
029import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson;
030import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkWithRevisionJson;
031import ca.uhn.fhir.rest.api.server.RequestDetails;
032import ca.uhn.fhir.rest.server.TransactionLogMessages;
033import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
034import ca.uhn.fhir.rest.server.provider.ProviderConstants;
035import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
036import ca.uhn.fhir.util.ParametersUtil;
037import jakarta.annotation.Nonnull;
038import jakarta.annotation.Nullable;
039import org.hl7.fhir.instance.model.api.IBase;
040import org.hl7.fhir.instance.model.api.IBaseParameters;
041import org.hl7.fhir.instance.model.api.IPrimitiveType;
042import org.springframework.data.domain.Page;
043
044import java.util.Arrays;
045import java.util.Collection;
046import java.util.Collections;
047import java.util.List;
048import java.util.Objects;
049import java.util.stream.Collectors;
050
051public abstract class BaseMdmProvider {
052
053        protected final FhirContext myFhirContext;
054
055        public BaseMdmProvider(FhirContext theFhirContext) {
056                myFhirContext = theFhirContext;
057        }
058
059        protected void validateMergeParameters(
060                        IPrimitiveType<String> theFromGoldenResourceId, IPrimitiveType<String> theToGoldenResourceId) {
061                validateNotNull(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId);
062                validateNotNull(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResourceId);
063                if (theFromGoldenResourceId.getValue().equals(theToGoldenResourceId.getValue())) {
064                        throw new InvalidRequestException(
065                                        Msg.code(1493) + "fromGoldenResourceId must be different from toGoldenResourceId");
066                }
067        }
068
069        private void validateNotNull(String theName, IPrimitiveType<String> theString) {
070                if (theString == null || theString.getValue() == null) {
071                        throw new InvalidRequestException(Msg.code(1494) + theName + " cannot be null");
072                }
073        }
074
075        protected void validateMdmLinkHistoryParameters(
076                        List<IPrimitiveType<String>> theGoldenResourceIds, List<IPrimitiveType<String>> theSourceIds) {
077                validateBothCannotBeNullOrEmpty(
078                                ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID,
079                                theGoldenResourceIds,
080                                ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID,
081                                theSourceIds);
082        }
083
084        private void validateBothCannotBeNullOrEmpty(
085                        String theFirstName,
086                        List<IPrimitiveType<String>> theFirstList,
087                        String theSecondName,
088                        List<IPrimitiveType<String>> theSecondList) {
089                if ((theFirstList == null || theFirstList.isEmpty()) && (theSecondList == null || theSecondList.isEmpty())) {
090                        throw new InvalidRequestException(Msg.code(2292) + "Please include either [" + theFirstName + "]s, ["
091                                        + theSecondName + "]s, or both in your search inputs.");
092                }
093        }
094
095        protected void validateUpdateLinkParameters(
096                        IPrimitiveType<String> theGoldenResourceId,
097                        IPrimitiveType<String> theResourceId,
098                        IPrimitiveType<String> theMatchResult) {
099                validateNotNull(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId);
100                validateNotNull(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theResourceId);
101                validateNotNull(ProviderConstants.MDM_UPDATE_LINK_MATCH_RESULT, theMatchResult);
102                MdmMatchResultEnum matchResult = MdmMatchResultEnum.valueOf(theMatchResult.getValue());
103                switch (matchResult) {
104                        case NO_MATCH:
105                        case MATCH:
106                                break;
107                        default:
108                                throw new InvalidRequestException(Msg.code(1495) + ProviderConstants.MDM_UPDATE_LINK + " illegal "
109                                                + ProviderConstants.MDM_UPDATE_LINK_MATCH_RESULT + " value '" + matchResult + "'.  Must be "
110                                                + MdmMatchResultEnum.NO_MATCH + " or " + MdmMatchResultEnum.MATCH);
111                }
112        }
113
114        protected void validateNotDuplicateParameters(
115                        IPrimitiveType<String> theGoldenResourceId, IPrimitiveType<String> theResourceId) {
116                validateNotNull(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId);
117                validateNotNull(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theResourceId);
118        }
119
120        protected void validateCreateLinkParameters(
121                        IPrimitiveType<String> theGoldenResourceId,
122                        IPrimitiveType<String> theResourceId,
123                        @Nullable IPrimitiveType<String> theMatchResult) {
124                validateNotNull(ProviderConstants.MDM_CREATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId);
125                validateNotNull(ProviderConstants.MDM_CREATE_LINK_RESOURCE_ID, theResourceId);
126                if (theMatchResult != null) {
127                        MdmMatchResultEnum matchResult = MdmMatchResultEnum.valueOf(theMatchResult.getValue());
128                        switch (matchResult) {
129                                case NO_MATCH:
130                                case POSSIBLE_MATCH:
131                                case MATCH:
132                                        break;
133                                default:
134                                        throw new InvalidRequestException(Msg.code(1496) + ProviderConstants.MDM_CREATE_LINK + " illegal "
135                                                        + ProviderConstants.MDM_CREATE_LINK_MATCH_RESULT + " value '" + matchResult + "'.  Must be "
136                                                        + MdmMatchResultEnum.NO_MATCH + ", " + MdmMatchResultEnum.MATCH + " or "
137                                                        + MdmMatchResultEnum.POSSIBLE_MATCH);
138                        }
139                }
140        }
141
142        protected MdmTransactionContext createMdmContext(
143                        RequestDetails theRequestDetails,
144                        MdmTransactionContext.OperationType theOperationType,
145                        String theResourceType) {
146                TransactionLogMessages transactionLogMessages =
147                                TransactionLogMessages.createFromTransactionGuid(theRequestDetails.getTransactionGuid());
148                MdmTransactionContext mdmTransactionContext =
149                                new MdmTransactionContext(transactionLogMessages, theOperationType);
150                mdmTransactionContext.setResourceType(theResourceType);
151                return mdmTransactionContext;
152        }
153
154        @Nonnull
155        protected List<String> convertToStringsIncludingCommaDelimitedIfNotNull(
156                        List<IPrimitiveType<String>> thePrimitiveTypeStrings) {
157                if (thePrimitiveTypeStrings == null) {
158                        return Collections.emptyList();
159                }
160
161                return thePrimitiveTypeStrings.stream()
162                                .map(this::extractStringOrNull)
163                                .filter(Objects::nonNull)
164                                .map(input -> Arrays.asList(input.split(",")))
165                                .flatMap(Collection::stream)
166                                .collect(Collectors.toUnmodifiableList());
167        }
168
169        protected String extractStringOrNull(IPrimitiveType<String> theString) {
170                if (theString == null) {
171                        return null;
172                }
173                return theString.getValue();
174        }
175
176        protected IBaseParameters parametersFromMdmLinks(
177                        Page<MdmLinkJson> theMdmLinkStream,
178                        boolean theIncludeResultAndSource,
179                        ServletRequestDetails theServletRequestDetails,
180                        MdmPageRequest thePageRequest) {
181                IBaseParameters retval = ParametersUtil.newInstance(myFhirContext);
182                addPagingParameters(retval, theMdmLinkStream, theServletRequestDetails, thePageRequest);
183
184                long numDuplicates = theMdmLinkStream.getTotalElements();
185                ParametersUtil.addParameterToParametersLong(myFhirContext, retval, "total", numDuplicates);
186
187                theMdmLinkStream.getContent().forEach(mdmLink -> {
188                        IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retval, "link");
189                        ParametersUtil.addPartString(myFhirContext, resultPart, "goldenResourceId", mdmLink.getGoldenResourceId());
190                        ParametersUtil.addPartString(myFhirContext, resultPart, "sourceResourceId", mdmLink.getSourceId());
191
192                        if (theIncludeResultAndSource) {
193                                ParametersUtil.addPartString(
194                                                myFhirContext,
195                                                resultPart,
196                                                "matchResult",
197                                                mdmLink.getMatchResult().name());
198                                ParametersUtil.addPartString(
199                                                myFhirContext,
200                                                resultPart,
201                                                "linkSource",
202                                                mdmLink.getLinkSource().name());
203                                ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", mdmLink.getEidMatch());
204                                ParametersUtil.addPartBoolean(
205                                                myFhirContext, resultPart, "hadToCreateNewResource", mdmLink.getLinkCreatedNewResource());
206                                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", mdmLink.getScore());
207                                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "linkCreated", (double)
208                                                mdmLink.getCreated().getTime());
209                                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "linkUpdated", (double)
210                                                mdmLink.getUpdated().getTime());
211                        }
212                });
213                return retval;
214        }
215
216        protected void parametersFromMdmLinkRevisions(
217                        IBaseParameters theRetVal, List<MdmLinkWithRevisionJson> theMdmLinkRevisions) {
218                if (theMdmLinkRevisions.isEmpty()) {
219                        final IBase resultPart = ParametersUtil.addParameterToParameters(
220                                        myFhirContext, theRetVal, "historical links not found for query parameters");
221
222                        ParametersUtil.addPartString(
223                                        myFhirContext, resultPart, "theResults", "historical links not found for query parameters");
224                }
225
226                theMdmLinkRevisions.forEach(mdmLinkRevision -> parametersFromMdmLinkRevision(theRetVal, mdmLinkRevision));
227        }
228
229        private void parametersFromMdmLinkRevision(IBaseParameters retVal, MdmLinkWithRevisionJson mdmLinkRevision) {
230                final IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retVal, "historical link");
231                final MdmLinkJson mdmLink = mdmLinkRevision.getMdmLink();
232
233                ParametersUtil.addPartString(myFhirContext, resultPart, "goldenResourceId", mdmLink.getGoldenResourceId());
234                ParametersUtil.addPartString(
235                                myFhirContext,
236                                resultPart,
237                                "revisionTimestamp",
238                                mdmLinkRevision.getRevisionTimestamp().toString());
239                ParametersUtil.addPartString(myFhirContext, resultPart, "sourceResourceId", mdmLink.getSourceId());
240                ParametersUtil.addPartString(
241                                myFhirContext,
242                                resultPart,
243                                "matchResult",
244                                mdmLink.getMatchResult().name());
245                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", mdmLink.getScore());
246                ParametersUtil.addPartString(
247                                myFhirContext, resultPart, "linkSource", mdmLink.getLinkSource().name());
248                ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", mdmLink.getEidMatch());
249                ParametersUtil.addPartBoolean(
250                                myFhirContext, resultPart, "hadToCreateNewResource", mdmLink.getLinkCreatedNewResource());
251                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", mdmLink.getScore());
252                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "linkCreated", (double)
253                                mdmLink.getCreated().getTime());
254                ParametersUtil.addPartDecimal(myFhirContext, resultPart, "linkUpdated", (double)
255                                mdmLink.getUpdated().getTime());
256        }
257
258        protected void addPagingParameters(
259                        IBaseParameters theParameters,
260                        Page<MdmLinkJson> theCurrentPage,
261                        ServletRequestDetails theServletRequestDetails,
262                        MdmPageRequest thePageRequest) {
263                MdmPageLinkTuple mdmPageLinkTuple =
264                                MdmPageLinkBuilder.buildMdmPageLinks(theServletRequestDetails, theCurrentPage, thePageRequest);
265
266                if (mdmPageLinkTuple.getPreviousLink().isPresent()) {
267                        ParametersUtil.addParameterToParametersUri(
268                                        myFhirContext,
269                                        theParameters,
270                                        "prev",
271                                        mdmPageLinkTuple.getPreviousLink().get());
272                }
273
274                ParametersUtil.addParameterToParametersUri(
275                                myFhirContext, theParameters, "self", mdmPageLinkTuple.getSelfLink());
276
277                if (mdmPageLinkTuple.getNextLink().isPresent()) {
278                        ParametersUtil.addParameterToParametersUri(
279                                        myFhirContext,
280                                        theParameters,
281                                        "next",
282                                        mdmPageLinkTuple.getNextLink().get());
283                }
284        }
285}