
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.interceptor; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.interceptor.api.Hook; 024import ca.uhn.fhir.interceptor.api.Pointcut; 025import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 026import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 027import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 028import ca.uhn.fhir.mdm.api.MdmConstants; 029import ca.uhn.fhir.mdm.log.Logs; 030import ca.uhn.fhir.mdm.svc.MdmSearchExpansionResults; 031import ca.uhn.fhir.mdm.svc.MdmSearchExpansionSvc; 032import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 035import ca.uhn.fhir.rest.param.ReferenceParam; 036import ca.uhn.fhir.rest.server.util.ICachedSearchDetails; 037import ca.uhn.fhir.util.FhirTerser; 038import ca.uhn.fhir.util.ResourceReferenceInfo; 039import org.hl7.fhir.instance.model.api.IAnyResource; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IIdType; 042import org.slf4j.Logger; 043import org.springframework.beans.factory.annotation.Autowired; 044 045import java.util.HashSet; 046import java.util.List; 047import java.util.Optional; 048import java.util.Set; 049import java.util.stream.Collectors; 050 051/** 052 * <b>This class is experimental and subject to change. Use with caution.</b> 053 * <p> 054 * This interceptor provides an "MDM Virtualized" endpoint, meaning that 055 * searches are expanded to include MDM-linked resources (including any 056 * linked golden resource, and also including any other resources linked 057 * to that golden resource). Searches for non-MDM resources which have 058 * a reference to an MDM resource will have their reference parameter 059 * expanded to include the golden and linked resources. 060 * </p> 061 * <p> 062 * In addition, responses are cleaned up so that only the golden resource 063 * is included in responses, and references to non-golden resources 064 * are rewritten. 065 * </p> 066 * <p> 067 * This interceptor does not modify data that is being stored/written 068 * in any way, it only modifies data that is being returned by the 069 * server. 070 * </p> 071 * 072 * @since 8.0.0 073 */ 074public class MdmReadVirtualizationInterceptor<P extends IResourcePersistentId<?>> { 075 private static final Logger ourMdmTroubleshootingLog = Logs.getMdmTroubleshootingLog(); 076 077 private static final String CURRENTLY_PROCESSING_FLAG = 078 MdmReadVirtualizationInterceptor.class.getName() + "_CURRENTLY_PROCESSING"; 079 080 private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER_NO_RES_ID = (paramName, param) -> { 081 boolean hasChain = false; 082 if (param instanceof ReferenceParam) { 083 hasChain = ((ReferenceParam) param).hasChain(); 084 } 085 return !hasChain && !IAnyResource.SP_RES_ID.equals(paramName); 086 }; 087 088 private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER_ALL = (paramName, param) -> { 089 boolean hasChain = false; 090 if (param instanceof ReferenceParam) { 091 hasChain = ((ReferenceParam) param).hasChain(); 092 } 093 return !hasChain; 094 }; 095 096 @Autowired 097 private FhirContext myFhirContext; 098 099 @Autowired 100 private DaoRegistry myDaoRegistry; 101 102 @Autowired 103 private MdmSearchExpansionSvc myMdmSearchExpansionSvc; 104 105 @Hook( 106 value = Pointcut.STORAGE_PRESEARCH_REGISTERED, 107 order = MdmConstants.ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR) 108 public void preSearchRegistered( 109 RequestDetails theRequestDetails, 110 SearchParameterMap theSearchParameterMap, 111 ICachedSearchDetails theSearchDetails) { 112 ourMdmTroubleshootingLog 113 .atTrace() 114 .setMessage("MDM virtualization original search: {}{}") 115 .addArgument(theRequestDetails.getResourceName()) 116 .addArgument(() -> theSearchParameterMap.toNormalizedQueryString(myFhirContext)) 117 .log(); 118 119 String resourceType = theSearchDetails.getResourceType(); 120 121 if (theSearchParameterMap.hasIncludes() || theSearchParameterMap.hasRevIncludes()) { 122 myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails( 123 resourceType, theRequestDetails, theSearchParameterMap, PARAM_TESTER_ALL); 124 } else { 125 // If we don't have any includes, it's not worth auto-expanding the _id parameter since we'll only end 126 // up filtering out the extra resources afterward 127 myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails( 128 resourceType, theRequestDetails, theSearchParameterMap, PARAM_TESTER_NO_RES_ID); 129 } 130 131 ourMdmTroubleshootingLog 132 .atDebug() 133 .setMessage("MDM virtualization remapped search: {}{}") 134 .addArgument(theRequestDetails.getResourceName()) 135 .addArgument(() -> theSearchParameterMap.toNormalizedQueryString(myFhirContext)) 136 .log(); 137 } 138 139 @Hook(Pointcut.STORAGE_PRESHOW_RESOURCES) 140 public void preShowResources(RequestDetails theRequestDetails, IPreResourceShowDetails theDetails) { 141 MdmSearchExpansionResults expansionResults = MdmSearchExpansionSvc.getCachedExpansionResults(theRequestDetails); 142 if (expansionResults == null) { 143 // This means the PRESEARCH hook didn't save anything, which probably means 144 // no RequestDetails is available 145 return; 146 } 147 148 if (theRequestDetails.getUserData().get(CURRENTLY_PROCESSING_FLAG) != null) { 149 // Avoid recursive calls 150 return; 151 } 152 153 /* 154 * If a resource being returned is a resource that was mdm-expanded, 155 * we'll replace that resource with the originally requested resource, 156 * making sure to avoid adding duplicates to the results. 157 */ 158 Set<IIdType> resourcesInBundle = new HashSet<>(); 159 for (int resourceIdx = 0; resourceIdx < theDetails.size(); resourceIdx++) { 160 IBaseResource resource = theDetails.getResource(resourceIdx); 161 IIdType id = resource.getIdElement().toUnqualifiedVersionless(); 162 Optional<IIdType> originalIdOpt = expansionResults.getOriginalIdForExpandedId(id); 163 if (originalIdOpt.isPresent()) { 164 IIdType originalId = originalIdOpt.get(); 165 if (resourcesInBundle.add(originalId)) { 166 IBaseResource originalResource = fetchResourceFromRepository(theRequestDetails, originalId); 167 theDetails.setResource(resourceIdx, originalResource); 168 } else { 169 theDetails.setResource(resourceIdx, null); 170 } 171 } else { 172 if (!resourcesInBundle.add(id)) { 173 theDetails.setResource(resourceIdx, null); 174 } 175 } 176 } 177 178 FhirTerser terser = myFhirContext.newTerser(); 179 for (int resourceIdx = 0; resourceIdx < theDetails.size(); resourceIdx++) { 180 IBaseResource resource = theDetails.getResource(resourceIdx); 181 if (resource != null) { 182 183 // Extract all the references in the resources we're returning 184 // in case we need to remap them to golden equivalents 185 List<ResourceReferenceInfo> referenceInfos = terser.getAllResourceReferences(resource); 186 for (ResourceReferenceInfo referenceInfo : referenceInfos) { 187 IIdType referenceId = referenceInfo 188 .getResourceReference() 189 .getReferenceElement() 190 .toUnqualifiedVersionless(); 191 if (referenceId.hasResourceType() 192 && referenceId.hasIdPart() 193 && !referenceId.isLocal() 194 && !referenceId.isUuid()) { 195 Optional<IIdType> nonExpandedId = expansionResults.getOriginalIdForExpandedId(referenceId); 196 if (nonExpandedId != null && nonExpandedId.isPresent()) { 197 ourMdmTroubleshootingLog.debug( 198 "MDM virtualization is replacing reference at {} value {} with {}", 199 referenceInfo.getName(), 200 referenceInfo.getResourceReference().getReferenceElement(), 201 nonExpandedId.get().getValue()); 202 referenceInfo 203 .getResourceReference() 204 .setReference(nonExpandedId.get().getValue()); 205 } 206 } 207 } 208 } 209 } 210 211 ourMdmTroubleshootingLog 212 .atTrace() 213 .setMessage("Returning resources: {}") 214 .addArgument(() -> theDetails.getAllResources().stream() 215 .map(t -> t.getIdElement().toUnqualifiedVersionless().getValue()) 216 .sorted() 217 .collect(Collectors.toList())) 218 .log(); 219 } 220 221 private IBaseResource fetchResourceFromRepository(RequestDetails theRequestDetails, IIdType originalId) { 222 IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(originalId.getResourceType()); 223 theRequestDetails.getUserData().put(CURRENTLY_PROCESSING_FLAG, Boolean.TRUE); 224 IBaseResource originalResource; 225 try { 226 originalResource = dao.read(originalId, theRequestDetails); 227 } finally { 228 theRequestDetails.getUserData().remove(CURRENTLY_PROCESSING_FLAG); 229 } 230 return originalResource; 231 } 232}