
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}