Skip to content

Commit 31f32d5

Browse files
authored
add undo-merge operation (#7146)
* add undo-merge operation
1 parent 35cc4d0 commit 31f32d5

File tree

32 files changed

+1783
-485
lines changed

32 files changed

+1783
-485
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: add
3+
issue: 7148
4+
title: "An operation named `$hapi.fhir.undo-merge` has been added for Patient resources. This operation restores the
5+
resources that were updated by a Patient `$merge` operation to their previous versions based on the Provenance resource
6+
created by the `$merge` operation."

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,7 @@ public IReplaceReferencesSvc replaceReferencesSvc(
10151015
theProvenanceSvc);
10161016
}
10171017

1018+
@Primary
10181019
@Bean
10191020
public ReplaceReferencesProvenanceSvc replaceReferencesProvenanceSvc(DaoRegistry theDaoRegistry) {
10201021
return new ReplaceReferencesProvenanceSvc(theDaoRegistry);

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,18 @@
3838
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
3939
import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc;
4040
import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
41+
import ca.uhn.fhir.jpa.provider.merge.MergeValidationService;
4142
import ca.uhn.fhir.jpa.provider.merge.PatientMergeProvider;
4243
import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService;
44+
import ca.uhn.fhir.jpa.provider.merge.ResourceUndoMergeService;
4345
import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl;
4446
import ca.uhn.fhir.jpa.term.TermVersionAdapterSvcR4;
4547
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
4648
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
4749
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
4850
import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc;
51+
import ca.uhn.fhir.merge.MergeProvenanceSvc;
52+
import ca.uhn.fhir.replacereferences.PreviousResourceVersionRestorer;
4953
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
5054
import org.hl7.fhir.r4.model.Bundle;
5155
import org.hl7.fhir.r4.model.Meta;
@@ -107,6 +111,16 @@ public ITermLoaderSvc termLoaderService(
107111
return new TermLoaderSvcImpl(theDeferredStorageSvc, theCodeSystemStorageSvc);
108112
}
109113

114+
@Bean
115+
public MergeValidationService mergeValidationService(FhirContext theFhirContext, DaoRegistry theDaoRegistry) {
116+
return new MergeValidationService(theFhirContext, theDaoRegistry);
117+
}
118+
119+
@Bean
120+
public MergeProvenanceSvc mergeProvenanceSvc(DaoRegistry theDaoRegistry) {
121+
return new MergeProvenanceSvc(theDaoRegistry);
122+
}
123+
110124
@Bean
111125
public ResourceMergeService resourceMergeService(
112126
DaoRegistry theDaoRegistry,
@@ -115,7 +129,9 @@ public ResourceMergeService resourceMergeService(
115129
IRequestPartitionHelperSvc theRequestPartitionHelperSvc,
116130
IJobCoordinator theJobCoordinator,
117131
Batch2TaskHelper theBatch2TaskHelper,
118-
JpaStorageSettings theStorageSettings) {
132+
JpaStorageSettings theStorageSettings,
133+
MergeValidationService theMergeValidationService,
134+
MergeProvenanceSvc theMergeProvenanceSvc) {
119135

120136
return new ResourceMergeService(
121137
theStorageSettings,
@@ -124,17 +140,39 @@ public ResourceMergeService resourceMergeService(
124140
theHapiTransactionService,
125141
theRequestPartitionHelperSvc,
126142
theJobCoordinator,
127-
theBatch2TaskHelper);
143+
theBatch2TaskHelper,
144+
theMergeValidationService,
145+
theMergeProvenanceSvc);
146+
}
147+
148+
@Bean
149+
public ResourceUndoMergeService resourceUndoMergeService(
150+
DaoRegistry theDaoRegistry,
151+
MergeProvenanceSvc theMergeProvenanceSvc,
152+
PreviousResourceVersionRestorer theResourceVersionRestorer,
153+
MergeValidationService theMergeValidationService,
154+
IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
155+
return new ResourceUndoMergeService(
156+
theDaoRegistry,
157+
theMergeProvenanceSvc,
158+
theResourceVersionRestorer,
159+
theMergeValidationService,
160+
theRequestPartitionHelperSvc);
128161
}
129162

130163
@Bean
131164
public PatientMergeProvider patientMergeProvider(
132165
FhirContext theFhirContext,
133166
DaoRegistry theDaoRegistry,
134167
ResourceMergeService theResourceMergeService,
168+
ResourceUndoMergeService theResourceUndoMergeService,
135169
IInterceptorBroadcaster theInterceptorBroadcaster) {
136170

137171
return new PatientMergeProvider(
138-
theFhirContext, theDaoRegistry, theResourceMergeService, theInterceptorBroadcaster);
172+
theFhirContext,
173+
theDaoRegistry,
174+
theResourceMergeService,
175+
theResourceUndoMergeService,
176+
theInterceptorBroadcaster);
139177
}
140178
}

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.slf4j.Logger;
4848
import org.slf4j.LoggerFactory;
4949

50+
import java.util.Collections;
5051
import java.util.Date;
5152
import java.util.List;
5253
import java.util.stream.Stream;
@@ -182,7 +183,8 @@ private IBaseParameters replaceReferencesPreferSync(
182183
List.of(result),
183184
startTime,
184185
theRequestDetails,
185-
theReplaceReferencesRequest.provenanceAgents);
186+
theReplaceReferencesRequest.provenanceAgents,
187+
Collections.emptyList());
186188
}
187189

188190
Parameters retval = new Parameters();
Lines changed: 14 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
2727
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
2828
import ca.uhn.fhir.model.api.IProvenanceAgent;
29-
import ca.uhn.fhir.util.CanonicalIdentifier;
30-
import org.hl7.fhir.instance.model.api.IBaseReference;
3129
import org.hl7.fhir.instance.model.api.IBaseResource;
3230
import org.hl7.fhir.r4.model.Patient;
3331

@@ -36,55 +34,17 @@
3634
/**
3735
* See <a href="https://build.fhir.org/patient-operation-merge.html">Patient $merge spec</a>
3836
*/
39-
public abstract class BaseMergeOperationInputParameters {
37+
public class MergeOperationInputParameters extends MergeOperationsCommonInputParameters {
4038

41-
private List<CanonicalIdentifier> mySourceResourceIdentifiers;
42-
private List<CanonicalIdentifier> myTargetResourceIdentifiers;
43-
private IBaseReference mySourceResource;
44-
private IBaseReference myTargetResource;
4539
private boolean myPreview;
4640
private boolean myDeleteSource;
4741
private IBaseResource myResultResource;
48-
private final int myResourceLimit;
4942
private List<IProvenanceAgent> myProvenanceAgents;
5043
private boolean myCreateProvenance = true;
44+
private IBaseResource myOriginalInputParameters;
5145

52-
protected BaseMergeOperationInputParameters(int theResourceLimit) {
53-
myResourceLimit = theResourceLimit;
54-
}
55-
56-
public abstract String getSourceResourceParameterName();
57-
58-
public abstract String getTargetResourceParameterName();
59-
60-
public abstract String getSourceIdentifiersParameterName();
61-
62-
public abstract String getTargetIdentifiersParameterName();
63-
64-
public abstract String getResultResourceParameterName();
65-
66-
public List<CanonicalIdentifier> getSourceIdentifiers() {
67-
return mySourceResourceIdentifiers;
68-
}
69-
70-
public boolean hasAtLeastOneSourceIdentifier() {
71-
return mySourceResourceIdentifiers != null && !mySourceResourceIdentifiers.isEmpty();
72-
}
73-
74-
public void setSourceResourceIdentifiers(List<CanonicalIdentifier> theSourceIdentifiers) {
75-
this.mySourceResourceIdentifiers = theSourceIdentifiers;
76-
}
77-
78-
public List<CanonicalIdentifier> getTargetIdentifiers() {
79-
return myTargetResourceIdentifiers;
80-
}
81-
82-
public boolean hasAtLeastOneTargetIdentifier() {
83-
return myTargetResourceIdentifiers != null && !myTargetResourceIdentifiers.isEmpty();
84-
}
85-
86-
public void setTargetResourceIdentifiers(List<CanonicalIdentifier> theTargetIdentifiers) {
87-
this.myTargetResourceIdentifiers = theTargetIdentifiers;
46+
protected MergeOperationInputParameters(int theResourceLimit) {
47+
super(theResourceLimit);
8848
}
8949

9050
public boolean getPreview() {
@@ -111,26 +71,6 @@ public void setResultResource(IBaseResource theResultResource) {
11171
this.myResultResource = theResultResource;
11272
}
11373

114-
public IBaseReference getSourceResource() {
115-
return mySourceResource;
116-
}
117-
118-
public void setSourceResource(IBaseReference theSourceResource) {
119-
this.mySourceResource = theSourceResource;
120-
}
121-
122-
public IBaseReference getTargetResource() {
123-
return myTargetResource;
124-
}
125-
126-
public void setTargetResource(IBaseReference theTargetResource) {
127-
this.myTargetResource = theTargetResource;
128-
}
129-
130-
public int getResourceLimit() {
131-
return myResourceLimit;
132-
}
133-
13474
public boolean getCreateProvenance() {
13575
return myCreateProvenance;
13676
}
@@ -147,17 +87,23 @@ public void setProvenanceAgents(List<IProvenanceAgent> theProvenanceAgents) {
14787
this.myProvenanceAgents = theProvenanceAgents;
14888
}
14989

90+
public IBaseResource getOriginalInputParameters() {
91+
return myOriginalInputParameters;
92+
}
93+
94+
public void setOriginalInputParameters(IBaseResource theOriginalInputParameters) {
95+
myOriginalInputParameters = theOriginalInputParameters;
96+
}
97+
15098
public MergeJobParameters asMergeJobParameters(
15199
FhirContext theFhirContext,
152100
JpaStorageSettings theStorageSettings,
153101
Patient theSourceResource,
154102
Patient theTargetResource,
155103
RequestPartitionId thePartitionId) {
156104
MergeJobParameters retval = new MergeJobParameters();
157-
if (getResultResource() != null) {
158-
retval.setResultResource(theFhirContext.newJsonParser().encodeResourceToString(getResultResource()));
159-
}
160-
retval.setDeleteSource(getDeleteSource());
105+
retval.setOriginalInputParameters(
106+
theFhirContext.newJsonParser().encodeResourceToString(myOriginalInputParameters));
161107
retval.setBatchSize(theStorageSettings.getDefaultTransactionEntriesForWrite());
162108
retval.setSourceId(new FhirIdJson(theSourceResource.getIdElement().toVersionless()));
163109
retval.setTargetId(new FhirIdJson(theTargetResource.getIdElement().toVersionless()));

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,16 @@
1919
*/
2020
package ca.uhn.fhir.jpa.provider.merge;
2121

22-
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
2322
import org.hl7.fhir.instance.model.api.IBaseResource;
2423

2524
/**
2625
* See <a href="https://build.fhir.org/patient-operation-merge.html">Patient $merge spec</a>
2726
*/
28-
public class MergeOperationOutcome {
29-
private IBaseOperationOutcome myOperationOutcome;
30-
private int myHttpStatusCode;
27+
public class MergeOperationOutcome extends OperationOutcomeWithStatusCode {
28+
3129
private IBaseResource myUpdatedTargetResource;
3230
private IBaseResource myTask;
3331

34-
public IBaseOperationOutcome getOperationOutcome() {
35-
return myOperationOutcome;
36-
}
37-
38-
public void setOperationOutcome(IBaseOperationOutcome theOperationOutcome) {
39-
this.myOperationOutcome = theOperationOutcome;
40-
}
41-
42-
public int getHttpStatusCode() {
43-
return myHttpStatusCode;
44-
}
45-
46-
public void setHttpStatusCode(int theHttpStatusCode) {
47-
this.myHttpStatusCode = theHttpStatusCode;
48-
}
49-
5032
public IBaseResource getUpdatedTargetResource() {
5133
return myUpdatedTargetResource;
5234
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*-
2+
* #%L
3+
* HAPI FHIR JPA Server
4+
* %%
5+
* Copyright (C) 2014 - 2025 Smile CDR, Inc.
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package ca.uhn.fhir.jpa.provider.merge;
21+
22+
import ca.uhn.fhir.util.CanonicalIdentifier;
23+
import org.hl7.fhir.instance.model.api.IBaseReference;
24+
25+
import java.util.List;
26+
27+
/**
28+
* Class for input parameters used in both $merge and $hapi.fhir.undo-merge operations.
29+
*/
30+
public class MergeOperationsCommonInputParameters {
31+
private List<CanonicalIdentifier> mySourceResourceIdentifiers;
32+
private List<CanonicalIdentifier> myTargetResourceIdentifiers;
33+
private IBaseReference mySourceResource;
34+
private IBaseReference myTargetResource;
35+
private final int myResourceLimit;
36+
37+
public MergeOperationsCommonInputParameters(int theResourceLimit) {
38+
myResourceLimit = theResourceLimit;
39+
}
40+
41+
public List<CanonicalIdentifier> getSourceIdentifiers() {
42+
return mySourceResourceIdentifiers;
43+
}
44+
45+
public boolean hasAtLeastOneSourceIdentifier() {
46+
return mySourceResourceIdentifiers != null && !mySourceResourceIdentifiers.isEmpty();
47+
}
48+
49+
public void setSourceResourceIdentifiers(List<CanonicalIdentifier> theSourceIdentifiers) {
50+
this.mySourceResourceIdentifiers = theSourceIdentifiers;
51+
}
52+
53+
public List<CanonicalIdentifier> getTargetIdentifiers() {
54+
return myTargetResourceIdentifiers;
55+
}
56+
57+
public boolean hasAtLeastOneTargetIdentifier() {
58+
return myTargetResourceIdentifiers != null && !myTargetResourceIdentifiers.isEmpty();
59+
}
60+
61+
public void setTargetResourceIdentifiers(List<CanonicalIdentifier> theTargetIdentifiers) {
62+
this.myTargetResourceIdentifiers = theTargetIdentifiers;
63+
}
64+
65+
public IBaseReference getSourceResource() {
66+
return mySourceResource;
67+
}
68+
69+
public void setSourceResource(IBaseReference theSourceResource) {
70+
this.mySourceResource = theSourceResource;
71+
}
72+
73+
public IBaseReference getTargetResource() {
74+
return myTargetResource;
75+
}
76+
77+
public void setTargetResource(IBaseReference theTargetResource) {
78+
this.myTargetResource = theTargetResource;
79+
}
80+
81+
public int getResourceLimit() {
82+
return myResourceLimit;
83+
}
84+
}

0 commit comments

Comments
 (0)