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