
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.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 MdmExpandersHolder myMdmExpandersHolder; 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 IMdmLinkExpandSvc mdmLinkExpandSvc = myMdmExpandersHolder.getLinkExpandSvcInstance(); 145 146 List<IQueryParameterType> toRemove = new ArrayList<>(); 147 List<IQueryParameterType> toAdd = new ArrayList<>(); 148 for (IQueryParameterType iQueryParameterType : orList) { 149 if (iQueryParameterType instanceof ReferenceParam) { 150 ReferenceParam refParam = (ReferenceParam) iQueryParameterType; 151 if (theParamTester.shouldExpand(theParamName, refParam)) { 152 ourLog.debug("Found a reference parameter to expand: {}", refParam); 153 // First, attempt to expand as a source resource. 154 IIdType sourceId = newId(refParam.getValue()); 155 Set<String> expandedResourceIds = 156 mdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, sourceId); 157 158 // If we failed, attempt to expand as a golden resource 159 if (expandedResourceIds.isEmpty()) { 160 expandedResourceIds = 161 mdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, sourceId); 162 } 163 164 // Rebuild the search param list. 165 if (!expandedResourceIds.isEmpty()) { 166 ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds)); 167 toRemove.add(refParam); 168 for (String resourceId : expandedResourceIds) { 169 IIdType nextReference = 170 newId(addResourceTypeIfNecessary(refParam.getResourceType(), resourceId)); 171 toAdd.add(new ReferenceParam(nextReference)); 172 theResultsToPopulate.addExpandedId(sourceId, nextReference); 173 } 174 } 175 } 176 } else if (theParamName.equalsIgnoreCase(IAnyResource.SP_RES_ID)) { 177 expandIdParameter( 178 theRequestPartitionId, 179 iQueryParameterType, 180 toAdd, 181 toRemove, 182 theParamTester, 183 theResourceName, 184 theResultsToPopulate); 185 } 186 } 187 188 orList.removeAll(toRemove); 189 orList.addAll(toAdd); 190 } 191 192 private IIdType newId(String value) { 193 return myFhirContext.getVersion().newIdType(value); 194 } 195 196 private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) { 197 if (theResourceId.contains("/") || isBlank(theResourceType)) { 198 return theResourceId; 199 } else { 200 return theResourceType + "/" + theResourceId; 201 } 202 } 203 204 /** 205 * Expands out the provided _id parameter into all the various 206 * ids of linked resources. 207 */ 208 private void expandIdParameter( 209 RequestPartitionId theRequestPartitionId, 210 IQueryParameterType theIdParameter, 211 List<IQueryParameterType> theAddList, 212 List<IQueryParameterType> theRemoveList, 213 IParamTester theParamTester, 214 String theResourceName, 215 MdmSearchExpansionResults theResultsToPopulate) { 216 // id parameters can either be StringParam (for $everything operation) 217 // or TokenParam (for searches) 218 // either case, we want to expand it out and grab all related resources 219 IIdType id; 220 Creator<? extends IQueryParameterType> creator; 221 boolean mdmExpand = false; 222 if (theIdParameter instanceof TokenParam) { 223 TokenParam param = (TokenParam) theIdParameter; 224 mdmExpand = theParamTester.shouldExpand(IAnyResource.SP_RES_ID, param); 225 String value = param.getValue(); 226 value = addResourceTypeIfNecessary(theResourceName, value); 227 id = newId(value); 228 creator = TokenParam::new; 229 } else { 230 creator = null; 231 id = null; 232 } 233 234 if (id == null) { 235 // in case the _id parameter type is different from the above 236 ourLog.warn( 237 "_id parameter of incorrect type. Expected StringParam or TokenParam, but got {}. No expansion will be done!", 238 theIdParameter.getClass().getSimpleName()); 239 } else if (mdmExpand) { 240 ourLog.debug("_id parameter must be expanded out from: {}", id.getValue()); 241 242 IMdmLinkExpandSvc mdmLinkExpandSvc = myMdmExpandersHolder.getLinkExpandSvcInstance(); 243 Set<String> expandedResourceIds = mdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id); 244 245 if (expandedResourceIds.isEmpty()) { 246 expandedResourceIds = mdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, id); 247 } 248 249 // Rebuild 250 if (!expandedResourceIds.isEmpty()) { 251 ourLog.debug("_id parameter has been expanded to: {}", expandedResourceIds); 252 253 // remove the original 254 theRemoveList.add(theIdParameter); 255 256 // add in all the linked values 257 expandedResourceIds.stream().map(creator::create).forEach(theAddList::add); 258 259 for (String expandedId : expandedResourceIds) { 260 theResultsToPopulate.addExpandedId( 261 id, newId(addResourceTypeIfNecessary(theResourceName, expandedId))); 262 } 263 } 264 } 265 // else - no expansion required 266 } 267 268 // A simple interface to turn ids into some form of IQueryParameterTypes 269 private interface Creator<T extends IQueryParameterType> { 270 T create(String id); 271 } 272 273 @FunctionalInterface 274 public interface IParamTester { 275 276 boolean shouldExpand(String theParamName, BaseParam theParam); 277 } 278 279 @Nullable 280 public static MdmSearchExpansionResults getCachedExpansionResults(@Nonnull RequestDetails theRequestDetails) { 281 MdmSearchExpansionResults expansionResults = 282 (MdmSearchExpansionResults) theRequestDetails.getUserData().get(EXPANSION_RESULTS); 283 return expansionResults; 284 } 285}