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.ResourceVersionConflictException;
030import org.hl7.fhir.instance.model.api.IBaseResource;
031import org.hl7.fhir.instance.model.api.IIdType;
032import org.hl7.fhir.r4.model.Reference;
033
034import java.util.List;
035
036/**
037 * This is a class to restore resources to their previous versions based on the provided versioned resource references.
038 * It is used in the context of undoing changes made by the $hapi.fhir.replace-references operation.
039 */
040public class PreviousResourceVersionRestorer {
041
042        private final HapiTransactionService myHapiTransactionService;
043        private final DaoRegistry myDaoRegistry;
044
045        public PreviousResourceVersionRestorer(
046                        DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService) {
047                myDaoRegistry = theDaoRegistry;
048                myHapiTransactionService = theHapiTransactionService;
049        }
050
051        /**
052         * Given a list of versioned resource references, this method restores each resource to its previous version
053         * if the resource's current version is the same as specified in the given reference
054         * (i.e. the resource was not updated since the reference was created).
055         *
056         * This method is transactional and will attempt to restore all resources in a single transaction.
057         *
058         * Note that this method updates a resource using its previous version's content,
059         * so it will actually cause a new version to be created (i.e. it does not rewrite the history).
060         *
061         * @throws IllegalArgumentException if a given reference is versionless
062         * @throws IllegalArgumentException a given reference has version 1, so it cannot have a previous version to restore to.
063         * @throws ResourceVersionConflictException if the current version of the resource does not match the version specified in the reference.
064         */
065        public void restoreToPreviousVersionsInTrx(
066                        List<Reference> theReferences, RequestDetails theRequestDetails, RequestPartitionId thePartitionId) {
067                myHapiTransactionService
068                                .withRequest(theRequestDetails)
069                                .withRequestPartitionId(thePartitionId)
070                                .execute(() -> restoreToPreviousVersions(theReferences, theRequestDetails));
071        }
072
073        private void restoreToPreviousVersions(List<Reference> theReferences, RequestDetails theRequestDetails) {
074                for (Reference reference : theReferences) {
075                        String referenceStr = reference.getReference();
076                        IIdType referenceId = new IdDt(referenceStr);
077
078                        if (!referenceId.hasVersionIdPart()) {
079                                throw new IllegalArgumentException(
080                                                Msg.code(2730) + "Reference does not have a version: " + referenceStr);
081                        }
082                        Long referenceVersion = referenceId.getVersionIdPartAsLong();
083
084                        // Restore previous version (version - 1)
085                        long previousVersion = referenceVersion - 1;
086                        if (previousVersion < 1) {
087                                throw new IllegalArgumentException(Msg.code(2731)
088                                                + "Resource cannot be restored to a previous as the provided version is 1: " + referenceStr);
089                        }
090
091                        // Read the current resource
092                        IFhirResourceDao<IBaseResource> dao = myDaoRegistry.getResourceDao(referenceId.getResourceType());
093                        IBaseResource currentResource = dao.read(referenceId.toUnqualifiedVersionless(), theRequestDetails);
094
095                        // Check current version
096                        Long currentVersion = currentResource.getIdElement().getVersionIdPartAsLong();
097                        if (!currentVersion.equals(referenceVersion)) {
098                                String msg = String.format(
099                                                "The resource cannot be restored because the current version of resource %s (%s) does not match the expected version (%s)",
100                                                referenceStr, currentVersion, referenceVersion);
101                                throw new ResourceVersionConflictException(Msg.code(2732) + msg);
102                        }
103
104                        IIdType previousId = referenceId.withVersion(Long.toString(previousVersion));
105                        IBaseResource previousResource = dao.read(previousId, theRequestDetails);
106                        previousResource.setId(previousResource.getIdElement().toUnqualifiedVersionless());
107
108                        // Update the resource to the previous version's content
109                        dao.update(previousResource, theRequestDetails);
110                }
111        }
112}