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