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}