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.svc;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
025import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
026import ca.uhn.fhir.mdm.api.IMdmLinkExpandSvc;
027import ca.uhn.fhir.mdm.log.Logs;
028import ca.uhn.fhir.model.api.IQueryParameterType;
029import ca.uhn.fhir.rest.api.server.RequestDetails;
030import ca.uhn.fhir.rest.param.BaseParam;
031import ca.uhn.fhir.rest.param.ReferenceParam;
032import ca.uhn.fhir.rest.param.TokenParam;
033import jakarta.annotation.Nonnull;
034import jakarta.annotation.Nullable;
035import org.hl7.fhir.instance.model.api.IAnyResource;
036import org.hl7.fhir.instance.model.api.IIdType;
037import org.slf4j.Logger;
038import org.springframework.beans.factory.annotation.Autowired;
039
040import java.util.ArrayList;
041import java.util.List;
042import java.util.Map;
043import java.util.Objects;
044import java.util.Set;
045
046import static org.apache.commons.lang3.StringUtils.isBlank;
047
048public class MdmSearchExpansionSvc {
049        private static final String EXPANSION_RESULTS = MdmSearchExpansionSvc.class.getName() + "_EXPANSION_RESULTS";
050        private static final String RESOURCE_NAME = MdmSearchExpansionSvc.class.getName() + "_RESOURCE_NAME";
051        private static final String QUERY_STRING = MdmSearchExpansionSvc.class.getName() + "_QUERY_STRING";
052        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
053
054        @Autowired
055        private FhirContext myFhirContext;
056
057        @Autowired
058        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
059
060        @Autowired
061        private IMdmLinkExpandSvc myMdmLinkExpandSvc;
062
063        /**
064         * This method looks through all the reference parameters within a {@link SearchParameterMap}
065         * and performs MDM expansion. This means looking for any subject/patient parameters, and
066         * expanding them to include any linked and golden resource patients. So, for example, a
067         * search for <code>Encounter?subject=Patient/1</code> might be modified to be a search
068         * for <code>Encounter?subject=Patient/1,Patient/999</code> if 999 is linked to 1 by MDM.
069         * <p>
070         * This is an internal MDM service and its API is subject to change. Use with caution!
071         * </p>
072         *
073         * @param theRequestDetails     The incoming request details
074         * @param theSearchParameterMap The parameter map to modify
075         * @param theParamTester        Determines which parameters should be expanded
076         * @return Returns the results of the expansion, which are also stored in the {@link RequestDetails} userdata map, so this service will only be invoked a maximum of once per request.
077         * @since 8.0.0
078         */
079        public MdmSearchExpansionResults expandSearchAndStoreInRequestDetails(
080                        String theResourceName,
081                        @Nullable RequestDetails theRequestDetails,
082                        @Nonnull SearchParameterMap theSearchParameterMap,
083                        IParamTester theParamTester) {
084
085                if (theRequestDetails == null) {
086                        return null;
087                }
088
089                // Try to detect if the RequestDetails is being reused across multiple different queries, which
090                // can happen during CQL measure evaluation
091                {
092                        String resourceName = theRequestDetails.getResourceName();
093                        String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext);
094                        if (!Objects.equals(resourceName, theRequestDetails.getUserData().get(RESOURCE_NAME))
095                                        || !Objects.equals(
096                                                        queryString, theRequestDetails.getUserData().get(QUERY_STRING))) {
097                                theRequestDetails.getUserData().remove(EXPANSION_RESULTS);
098                        }
099                }
100
101                MdmSearchExpansionResults expansionResults = getCachedExpansionResults(theRequestDetails);
102                if (expansionResults != null) {
103                        return expansionResults;
104                }
105
106                expansionResults = new MdmSearchExpansionResults();
107
108                final RequestPartitionId requestPartitionId =
109                                myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
110                                                theRequestDetails, theRequestDetails.getResourceName(), theSearchParameterMap);
111
112                for (Map.Entry<String, List<List<IQueryParameterType>>> set : theSearchParameterMap.entrySet()) {
113                        String paramName = set.getKey();
114                        List<List<IQueryParameterType>> andList = set.getValue();
115                        for (List<IQueryParameterType> orList : andList) {
116                                // here we will know if it's an _id param or not
117                                // from theSearchParameterMap.keySet()
118                                expandAnyReferenceParameters(
119                                                requestPartitionId, theResourceName, paramName, orList, theParamTester, expansionResults);
120                        }
121                }
122
123                theRequestDetails.getUserData().put(EXPANSION_RESULTS, expansionResults);
124
125                /*
126                 * Note: Do this at the end so that the query string reflects the post-translated
127                 * query string
128                 */
129                String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext);
130                theRequestDetails.getUserData().put(RESOURCE_NAME, theResourceName);
131                theRequestDetails.getUserData().put(QUERY_STRING, queryString);
132
133                return expansionResults;
134        }
135
136        private void expandAnyReferenceParameters(
137                        RequestPartitionId theRequestPartitionId,
138                        String theResourceName,
139                        String theParamName,
140                        List<IQueryParameterType> orList,
141                        IParamTester theParamTester,
142                        MdmSearchExpansionResults theResultsToPopulate) {
143
144                List<IQueryParameterType> toRemove = new ArrayList<>();
145                List<IQueryParameterType> toAdd = new ArrayList<>();
146                for (IQueryParameterType iQueryParameterType : orList) {
147                        if (iQueryParameterType instanceof ReferenceParam) {
148                                ReferenceParam refParam = (ReferenceParam) iQueryParameterType;
149                                if (theParamTester.shouldExpand(theParamName, refParam)) {
150                                        ourLog.debug("Found a reference parameter to expand: {}", refParam);
151                                        // First, attempt to expand as a source resource.
152                                        IIdType sourceId = newId(refParam.getValue());
153                                        Set<String> expandedResourceIds =
154                                                        myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, sourceId);
155
156                                        // If we failed, attempt to expand as a golden resource
157                                        if (expandedResourceIds.isEmpty()) {
158                                                expandedResourceIds =
159                                                                myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, sourceId);
160                                        }
161
162                                        // Rebuild the search param list.
163                                        if (!expandedResourceIds.isEmpty()) {
164                                                ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
165                                                toRemove.add(refParam);
166                                                for (String resourceId : expandedResourceIds) {
167                                                        IIdType nextReference =
168                                                                        newId(addResourceTypeIfNecessary(refParam.getResourceType(), resourceId));
169                                                        toAdd.add(new ReferenceParam(nextReference));
170                                                        theResultsToPopulate.addExpandedId(sourceId, nextReference);
171                                                }
172                                        }
173                                }
174                        } else if (theParamName.equalsIgnoreCase(IAnyResource.SP_RES_ID)) {
175                                expandIdParameter(
176                                                theRequestPartitionId,
177                                                iQueryParameterType,
178                                                toAdd,
179                                                toRemove,
180                                                theParamTester,
181                                                theResourceName,
182                                                theResultsToPopulate);
183                        }
184                }
185
186                orList.removeAll(toRemove);
187                orList.addAll(toAdd);
188        }
189
190        private IIdType newId(String value) {
191                return myFhirContext.getVersion().newIdType(value);
192        }
193
194        private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) {
195                if (theResourceId.contains("/") || isBlank(theResourceType)) {
196                        return theResourceId;
197                } else {
198                        return theResourceType + "/" + theResourceId;
199                }
200        }
201
202        /**
203         * Expands out the provided _id parameter into all the various
204         * ids of linked resources.
205         */
206        private void expandIdParameter(
207                        RequestPartitionId theRequestPartitionId,
208                        IQueryParameterType theIdParameter,
209                        List<IQueryParameterType> theAddList,
210                        List<IQueryParameterType> theRemoveList,
211                        IParamTester theParamTester,
212                        String theResourceName,
213                        MdmSearchExpansionResults theResultsToPopulate) {
214                // id parameters can either be StringParam (for $everything operation)
215                // or TokenParam (for searches)
216                // either case, we want to expand it out and grab all related resources
217                IIdType id;
218                Creator<? extends IQueryParameterType> creator;
219                boolean mdmExpand = false;
220                if (theIdParameter instanceof TokenParam) {
221                        TokenParam param = (TokenParam) theIdParameter;
222                        mdmExpand = theParamTester.shouldExpand(IAnyResource.SP_RES_ID, param);
223                        String value = param.getValue();
224                        value = addResourceTypeIfNecessary(theResourceName, value);
225                        id = newId(value);
226                        creator = TokenParam::new;
227                } else {
228                        creator = null;
229                        id = null;
230                }
231
232                if (id == null) {
233                        // in case the _id parameter type is different from the above
234                        ourLog.warn(
235                                        "_id parameter of incorrect type. Expected StringParam or TokenParam, but got {}. No expansion will be done!",
236                                        theIdParameter.getClass().getSimpleName());
237                } else if (mdmExpand) {
238                        ourLog.debug("_id parameter must be expanded out from: {}", id.getValue());
239
240                        Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id);
241
242                        if (expandedResourceIds.isEmpty()) {
243                                expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, id);
244                        }
245
246                        // Rebuild
247                        if (!expandedResourceIds.isEmpty()) {
248                                ourLog.debug("_id parameter has been expanded to: {}", expandedResourceIds);
249
250                                // remove the original
251                                theRemoveList.add(theIdParameter);
252
253                                // add in all the linked values
254                                expandedResourceIds.stream().map(creator::create).forEach(theAddList::add);
255
256                                for (String expandedId : expandedResourceIds) {
257                                        theResultsToPopulate.addExpandedId(
258                                                        id, newId(addResourceTypeIfNecessary(theResourceName, expandedId)));
259                                }
260                        }
261                }
262                // else - no expansion required
263        }
264
265        // A simple interface to turn ids into some form of IQueryParameterTypes
266        private interface Creator<T extends IQueryParameterType> {
267                T create(String id);
268        }
269
270        @FunctionalInterface
271        public interface IParamTester {
272
273                boolean shouldExpand(String theParamName, BaseParam theParam);
274        }
275
276        @Nullable
277        public static MdmSearchExpansionResults getCachedExpansionResults(@Nonnull RequestDetails theRequestDetails) {
278                MdmSearchExpansionResults expansionResults =
279                                (MdmSearchExpansionResults) theRequestDetails.getUserData().get(EXPANSION_RESULTS);
280                return expansionResults;
281        }
282}