001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.jpa.interceptor; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.fhirpath.IFhirPath; 024import ca.uhn.fhir.interceptor.api.Hook; 025import ca.uhn.fhir.interceptor.api.Interceptor; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 028import ca.uhn.fhir.jpa.api.model.DeleteConflict; 029import ca.uhn.fhir.jpa.api.model.DeleteConflictList; 030import ca.uhn.fhir.model.primitive.IdDt; 031import ca.uhn.fhir.rest.api.server.RequestDetails; 032import org.hl7.fhir.instance.model.api.IBaseReference; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036import org.springframework.beans.factory.annotation.Autowired; 037 038import java.util.HashSet; 039import java.util.List; 040import java.util.Objects; 041import java.util.Set; 042 043/** 044 * This JPA interceptor can be configured with a collection of FHIRPath expressions, and will disable 045 * referential integrity for target resources at those paths. 046 * <p> 047 * For example, suppose this interceptor is configured with a path of <code>AuditEvent.entity.what</code>, 048 * and an AuditEvent resource exists in the repository that has a reference in that path to resource 049 * <code>Patient/123</code>. Normally this reference would prevent the Patient resource from being deleted unless 050 * the AuditEvent was first deleted as well (or a <a href="/hapi-fhir/docs/server_jpa/configuration.html#cascading-deletes">cascading delete</a> was used). 051 * With this interceptor in place, the Patient resource could be deleted, and the AuditEvent would remain intact. 052 * </p> 053 */ 054@Interceptor 055public class OverridePathBasedReferentialIntegrityForDeletesInterceptor { 056 057 private static final Logger ourLog = 058 LoggerFactory.getLogger(OverridePathBasedReferentialIntegrityForDeletesInterceptor.class); 059 private final Set<String> myPaths = new HashSet<>(); 060 061 @Autowired 062 private FhirContext myFhirContext; 063 064 @Autowired 065 private DaoRegistry myDaoRegistry; 066 067 /** 068 * Constructor 069 */ 070 public OverridePathBasedReferentialIntegrityForDeletesInterceptor() { 071 super(); 072 } 073 074 /** 075 * Adds a FHIRPath expression indicating a resource path that should be ignored when considering referential 076 * integrity for deletes. 077 * 078 * @param thePath The FHIRPath expression, e.g. <code>AuditEvent.agent.who</code> 079 */ 080 public void addPath(String thePath) { 081 getPaths().add(thePath); 082 } 083 084 /** 085 * Remove all paths registered to this interceptor 086 */ 087 public void clearPaths() { 088 getPaths().clear(); 089 } 090 091 /** 092 * Returns the paths that will be considered by this interceptor 093 * 094 * @see #addPath(String) 095 */ 096 public Set<String> getPaths() { 097 return myPaths; 098 } 099 100 /** 101 * Interceptor hook method. Do not invoke directly. 102 */ 103 @Hook( 104 value = Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, 105 order = CascadingDeleteInterceptor.OVERRIDE_PATH_BASED_REF_INTEGRITY_INTERCEPTOR_ORDER) 106 public void handleDeleteConflicts(DeleteConflictList theDeleteConflictList, RequestDetails requestDetails) { 107 for (DeleteConflict nextConflict : theDeleteConflictList) { 108 ourLog.info( 109 "Ignoring referential integrity deleting {} - Referred to from {} at path {}", 110 nextConflict.getTargetId(), 111 nextConflict.getSourceId(), 112 nextConflict.getSourcePath()); 113 114 IdDt sourceId = nextConflict.getSourceId(); 115 IdDt targetId = nextConflict.getTargetId(); 116 String targetIdValue = targetId.toVersionless().getValue(); 117 118 IBaseResource sourceResource = 119 myDaoRegistry.getResourceDao(sourceId.getResourceType()).read(sourceId, requestDetails); 120 121 IFhirPath fhirPath = myFhirContext.newFhirPath(); 122 for (String nextPath : myPaths) { 123 List<IBaseReference> selections = fhirPath.evaluate(sourceResource, nextPath, IBaseReference.class); 124 for (IBaseReference nextSelection : selections) { 125 String selectionTargetValue = 126 nextSelection.getReferenceElement().toVersionless().getValue(); 127 if (Objects.equals(targetIdValue, selectionTargetValue)) { 128 theDeleteConflictList.setResourceIdToIgnoreConflict(nextConflict.getTargetId()); 129 break; 130 } 131 } 132 } 133 } 134 } 135}