
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.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 025import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 026import ca.uhn.fhir.rest.api.server.RequestDetails; 027import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 028import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 029import ca.uhn.fhir.rest.server.provider.ProviderConstants; 030import ca.uhn.fhir.util.OperationOutcomeUtil; 031import ca.uhn.fhir.util.ParametersUtil; 032import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 033import org.hl7.fhir.instance.model.api.IBaseParameters; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.hl7.fhir.instance.model.api.IIdType; 036import org.hl7.fhir.r4.model.Provenance; 037import org.hl7.fhir.r4.model.Reference; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041import java.util.Collections; 042import java.util.List; 043 044import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_UNDO_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; 045 046/** 047 * This service is implements the $hapi.fhir.replace-references operation. 048 * It reverts the changes made by $hapi.fhir.replace-references operation based on the Provenance resource 049 * that was created as part of the $hapi.fhir.replace-references operation. 050 * 051 * Current limitations: 052 * - It fails if any resources to be restored have been subsequently changed since the `$hapi.fhir.replace-references` operation was performed. 053 * - It can only run synchronously. 054 * - It fails if the number of resources to restore exceeds a specified resource limit 055 * (currently set to same size as getInternalSynchronousSearchSize in JPAStorageSettings by the operation provider). 056 */ 057public class UndoReplaceReferencesSvc { 058 059 private static final Logger ourLog = LoggerFactory.getLogger(UndoReplaceReferencesSvc.class); 060 061 private final ReplaceReferencesProvenanceSvc myReplaceReferencesProvenanceSvc; 062 private final PreviousResourceVersionRestorer myResourceVersionRestorer; 063 private final FhirContext myFhirContext; 064 private final DaoRegistry myDaoRegistry; 065 066 public UndoReplaceReferencesSvc( 067 DaoRegistry theDaoRegistry, 068 ReplaceReferencesProvenanceSvc theReplaceReferencesProvenanceSvc, 069 PreviousResourceVersionRestorer theResourceVersionRestorer) { 070 myDaoRegistry = theDaoRegistry; 071 myReplaceReferencesProvenanceSvc = theReplaceReferencesProvenanceSvc; 072 myResourceVersionRestorer = theResourceVersionRestorer; 073 myFhirContext = theDaoRegistry.getFhirContext(); 074 } 075 076 public IBaseParameters undoReplaceReferences( 077 UndoReplaceReferencesRequest theUndoReplaceReferencesRequest, RequestDetails theRequestDetails) { 078 079 // read source and target to ensure they still exist 080 readResource(theUndoReplaceReferencesRequest.sourceId, theRequestDetails); 081 readResource(theUndoReplaceReferencesRequest.targetId, theRequestDetails); 082 083 Provenance provenance = myReplaceReferencesProvenanceSvc.findProvenance( 084 theUndoReplaceReferencesRequest.targetId, 085 theUndoReplaceReferencesRequest.sourceId, 086 theRequestDetails, 087 ProviderConstants.OPERATION_UNDO_REPLACE_REFERENCES); 088 089 if (provenance == null) { 090 String msg = 091 "Unable to find a Provenance created by a $hapi.fhir.replace-references for the provided source and target IDs." 092 + " Ensure that IDs are correct and were previously used as parameters in a successful $hapi.fhir.replace-references operation"; 093 throw new ResourceNotFoundException(Msg.code(2728) + msg); 094 } 095 096 ourLog.info( 097 "Found Provenance resource with id: {} to be used for $undo-replace-references operation", 098 provenance.getIdElement().getValue()); 099 100 List<Reference> references = provenance.getTarget(); 101 // in replace-references operation provenance, the first two references are to the target and the source, 102 // and they are not updated as part of the operation so we should not restore their previous versions. 103 List<Reference> toRestore = references.subList(2, references.size()); 104 105 if (toRestore.size() > theUndoReplaceReferencesRequest.resourceLimit) { 106 String msg = String.format( 107 "Number of references to update (%d) exceeds the limit (%d)", 108 toRestore.size(), theUndoReplaceReferencesRequest.resourceLimit); 109 throw new InvalidRequestException(Msg.code(2729) + msg); 110 } 111 112 myResourceVersionRestorer.restoreToPreviousVersionsInTrx( 113 toRestore, Collections.emptySet(), theRequestDetails, theUndoReplaceReferencesRequest.partitionId); 114 115 IBaseOperationOutcome opOutcome = OperationOutcomeUtil.newInstance(myFhirContext); 116 String msg = String.format( 117 "Successfully restored %d resources to their previous versions based on the Provenance resource: %s", 118 toRestore.size(), provenance.getIdElement().getValue()); 119 OperationOutcomeUtil.addIssue(myFhirContext, opOutcome, "information", msg, null, null); 120 121 IBaseParameters outputParameters = ParametersUtil.newInstance(myFhirContext); 122 123 ParametersUtil.addParameterToParameters( 124 myFhirContext, outputParameters, OPERATION_UNDO_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME, opOutcome); 125 126 return outputParameters; 127 } 128 129 private IBaseResource readResource(IIdType theId, RequestDetails theRequestDetails) { 130 String resourceType = theId.getResourceType(); 131 IFhirResourceDao<IBaseResource> resourceDao = myDaoRegistry.getResourceDao(resourceType); 132 return resourceDao.read(theId, theRequestDetails); 133 } 134}