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.interceptor; 021 022import ca.uhn.fhir.interceptor.api.Hook; 023import ca.uhn.fhir.interceptor.api.Interceptor; 024import ca.uhn.fhir.interceptor.api.Pointcut; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 027import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 028import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 029import ca.uhn.fhir.mdm.api.IMdmLinkExpandSvc; 030import ca.uhn.fhir.mdm.log.Logs; 031import ca.uhn.fhir.model.api.IQueryParameterType; 032import ca.uhn.fhir.model.primitive.IdDt; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 035import ca.uhn.fhir.rest.param.ReferenceParam; 036import ca.uhn.fhir.rest.param.TokenParam; 037import org.hl7.fhir.instance.model.api.IIdType; 038import org.slf4j.Logger; 039import org.springframework.beans.factory.annotation.Autowired; 040 041import java.util.ArrayList; 042import java.util.List; 043import java.util.Map; 044import java.util.Set; 045 046/** 047 * This interceptor replaces the auto-generated CapabilityStatement that is generated 048 * by the HAPI FHIR Server with a static hard-coded resource. 049 */ 050@Interceptor 051public class MdmSearchExpandingInterceptor { 052 // A simple interface to turn ids into some form of IQueryParameterTypes 053 private interface Creator<T extends IQueryParameterType> { 054 T create(String id); 055 } 056 057 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 058 059 @Autowired 060 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 061 062 @Autowired 063 private IMdmLinkExpandSvc myMdmLinkExpandSvc; 064 065 @Autowired 066 private JpaStorageSettings myStorageSettings; 067 068 @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED) 069 public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { 070 071 if (myStorageSettings.isAllowMdmExpansion()) { 072 final RequestDetails requestDetailsToUse = 073 theRequestDetails == null ? new SystemRequestDetails() : theRequestDetails; 074 final RequestPartitionId requestPartitionId = 075 myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( 076 requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap); 077 for (Map.Entry<String, List<List<IQueryParameterType>>> set : theSearchParameterMap.entrySet()) { 078 String paramName = set.getKey(); 079 List<List<IQueryParameterType>> andList = set.getValue(); 080 for (List<IQueryParameterType> orList : andList) { 081 // here we will know if it's an _id param or not 082 // from theSearchParameterMap.keySet() 083 expandAnyReferenceParameters(requestPartitionId, paramName, orList); 084 } 085 } 086 } 087 } 088 089 /** 090 * If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion. 091 */ 092 private void expandAnyReferenceParameters( 093 RequestPartitionId theRequestPartitionId, String theParamName, List<IQueryParameterType> orList) { 094 List<IQueryParameterType> toRemove = new ArrayList<>(); 095 List<IQueryParameterType> toAdd = new ArrayList<>(); 096 for (IQueryParameterType iQueryParameterType : orList) { 097 if (iQueryParameterType instanceof ReferenceParam) { 098 ReferenceParam refParam = (ReferenceParam) iQueryParameterType; 099 if (refParam.isMdmExpand()) { 100 ourLog.debug("Found a reference parameter to expand: {}", refParam); 101 // First, attempt to expand as a source resource. 102 Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId( 103 theRequestPartitionId, new IdDt(refParam.getValue())); 104 105 // If we failed, attempt to expand as a golden resource 106 if (expandedResourceIds.isEmpty()) { 107 expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId( 108 theRequestPartitionId, new IdDt(refParam.getValue())); 109 } 110 111 // Rebuild the search param list. 112 if (!expandedResourceIds.isEmpty()) { 113 ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds)); 114 toRemove.add(refParam); 115 expandedResourceIds.stream() 116 .map(resourceId -> addResourceTypeIfNecessary(refParam.getResourceType(), resourceId)) 117 .map(ReferenceParam::new) 118 .forEach(toAdd::add); 119 } 120 } 121 } else if (theParamName.equalsIgnoreCase("_id")) { 122 expandIdParameter(theRequestPartitionId, iQueryParameterType, toAdd, toRemove); 123 } 124 } 125 126 orList.removeAll(toRemove); 127 orList.addAll(toAdd); 128 } 129 130 private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) { 131 if (theResourceId.contains("/")) { 132 return theResourceId; 133 } else { 134 return theResourceType + "/" + theResourceId; 135 } 136 } 137 138 /** 139 * Expands out the provided _id parameter into all the various 140 * ids of linked resources. 141 * 142 * @param theRequestPartitionId 143 * @param theIdParameter 144 * @param theAddList 145 * @param theRemoveList 146 */ 147 private void expandIdParameter( 148 RequestPartitionId theRequestPartitionId, 149 IQueryParameterType theIdParameter, 150 List<IQueryParameterType> theAddList, 151 List<IQueryParameterType> theRemoveList) { 152 // id parameters can either be StringParam (for $everything operation) 153 // or TokenParam (for searches) 154 // either case, we want to expand it out and grab all related resources 155 IIdType id; 156 Creator<? extends IQueryParameterType> creator; 157 boolean mdmExpand = false; 158 if (theIdParameter instanceof TokenParam) { 159 TokenParam param = (TokenParam) theIdParameter; 160 mdmExpand = param.isMdmExpand(); 161 id = new IdDt(param.getValue()); 162 creator = TokenParam::new; 163 } else { 164 creator = null; 165 id = null; 166 } 167 168 if (id == null) { 169 // in case the _id paramter type is different from the above 170 ourLog.warn( 171 "_id parameter of incorrect type. Expected StringParam or TokenParam, but got {}. No expansion will be done!", 172 theIdParameter.getClass().getSimpleName()); 173 } else if (mdmExpand) { 174 ourLog.debug("_id parameter must be expanded out from: {}", id.getValue()); 175 176 Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id); 177 178 if (expandedResourceIds.isEmpty()) { 179 expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, (IdDt) id); 180 } 181 182 // Rebuild 183 if (!expandedResourceIds.isEmpty()) { 184 ourLog.debug("_id parameter has been expanded to: {}", String.join(", ", expandedResourceIds)); 185 186 // remove the original 187 theRemoveList.add(theIdParameter); 188 189 // add in all the linked values 190 expandedResourceIds.stream().map(creator::create).forEach(theAddList::add); 191 } 192 } 193 // else - no expansion required 194 } 195}