001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.replacereferences;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
025import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
026import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
027import ca.uhn.fhir.model.primitive.IdDt;
028import ca.uhn.fhir.rest.api.server.RequestDetails;
029import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
030import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
031import org.hl7.fhir.instance.model.api.IBaseResource;
032import org.hl7.fhir.instance.model.api.IIdType;
033import org.hl7.fhir.r4.model.Reference;
034
035import java.util.List;
036import java.util.Set;
037
038/**
039 * This is a class to restore resources to their previous versions based on the provided versioned resource references.
040 * It is used in the context of undoing changes made by the $hapi.fhir.replace-references operation.
041 */
042public class PreviousResourceVersionRestorer {
043
044        private final HapiTransactionService myHapiTransactionService;
045        private final DaoRegistry myDaoRegistry;
046
047        public PreviousResourceVersionRestorer(
048                        DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService) {
049                myDaoRegistry = theDaoRegistry;
050                myHapiTransactionService = theHapiTransactionService;
051        }
052
053        /**
054         * Given a list of versioned resource references, this method restores each resource to its previous version
055         * if the resource's current version is the same as specified in the given reference
056         * (i.e. the resource was not updated since the reference was created).
057         *
058         * This method is transactional and will attempt to restore all resources in a single transaction.
059         *
060         * Note that this method updates a resource using its previous version's content,
061         * so it will actually cause a new version to be created (i.e. it does not rewrite the history).
062         * @param theReferences a list of versioned resource references to restore
063         * @param theReferencesToUndelete  the references that is expected to be deleted, and is ok to undelete
064         * @param theRequestDetails the request details for the operation
065         * @param thePartitionId the partition ID for the operation
066         *
067         * @throws IllegalArgumentException if a given reference is versionless
068         * @throws IllegalArgumentException a given reference has version 1, so it cannot have a previous version to restore to.
069         * @throws ResourceVersionConflictException if the current version of the resource does not match the version specified in the reference.
070         */
071        public void restoreToPreviousVersionsInTrx(
072                        List<Reference> theReferences,
073                        Set<Reference> theReferencesToUndelete,
074                        RequestDetails theRequestDetails,
075                        RequestPartitionId thePartitionId) {
076                myHapiTransactionService
077                                .withRequest(theRequestDetails)
078                                .withRequestPartitionId(thePartitionId)
079                                .execute(() -> restoreToPreviousVersions(theReferences, theReferencesToUndelete, theRequestDetails));
080        }
081
082        private void restoreToPreviousVersions(
083                        List<Reference> theReferences, Set<Reference> theReferencesToUndelete, RequestDetails theRequestDetails) {
084                for (Reference reference : theReferences) {
085                        String referenceStr = reference.getReference();
086                        IIdType referenceId = new IdDt(referenceStr);
087
088                        if (!referenceId.hasVersionIdPart()) {
089                                throw new IllegalArgumentException(
090                                                Msg.code(2730) + "Reference does not have a version: " + referenceStr);
091                        }
092                        Long referenceVersion = referenceId.getVersionIdPartAsLong();
093
094                        // Restore previous version (version - 1)
095                        long previousVersion = referenceVersion - 1;
096                        if (previousVersion < 1) {
097                                throw new IllegalArgumentException(Msg.code(2731)
098                                                + "Resource cannot be restored to a previous as the provided version is 1: " + referenceStr);
099                        }
100
101                        // Read the current resource
102                        IFhirResourceDao<IBaseResource> dao = myDaoRegistry.getResourceDao(referenceId.getResourceType());
103                        IBaseResource currentResource = null;
104                        try {
105                                currentResource = dao.read(referenceId.toUnqualifiedVersionless(), theRequestDetails);
106                        } catch (ResourceGoneException e) {
107                                if (!theReferencesToUndelete.contains(reference)) {
108                                        // enhance error message to include the reference of the deleted resource that cannot be restored
109                                        String msg = String.format(
110                                                        "The resource '%s' cannot be restored because it was deleted. %s",
111                                                        referenceStr, e.getMessage());
112                                        throw new ResourceGoneException(Msg.code(2751) + msg);
113                                }
114                        }
115
116                        // If resource exists, check its current version is the same as the provided reference version
117                        // if not, that means the resource was updated since the reference was created, so the operation should
118                        // fail.
119                        if (currentResource != null) {
120                                Long currentVersion = currentResource.getIdElement().getVersionIdPartAsLong();
121                                if (!currentVersion.equals(referenceVersion)) {
122                                        String msg = String.format(
123                                                        "The resource cannot be restored because the current version of resource %s (%s) does not match the expected version (%s)",
124                                                        referenceStr, currentVersion, referenceVersion);
125                                        throw new ResourceVersionConflictException(Msg.code(2732) + msg);
126                                }
127                        }
128
129                        IIdType previousId = referenceId.withVersion(Long.toString(previousVersion));
130                        IBaseResource previousResource = dao.read(previousId, theRequestDetails);
131                        previousResource.setId(previousResource.getIdElement().toUnqualifiedVersionless());
132
133                        // Update the resource to the previous version's content
134                        dao.update(previousResource, theRequestDetails);
135                }
136        }
137}