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}