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