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}