Add replace-references operation (#6526)

* Rehoming operation

* Rename operation

* Recover state before moving function to smile
Batch job artifacts are very preliminary

* Rename rehome to replace_references

* Rename rehome to replace_references

* spotless

* Add message code

* Fix op name

* Implement plain provider

* Add msg code

* Fix parameter types fhir version compatibility

* spotless

* Deduplicate code

* Update hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java

Implement suggestion

Co-authored-by: Ken Stevens <khstevens@gmail.com>

* Implement review request: move constant

* Change operation interface to use parameters for both source and target reference ids

* spotless

* Add revision requested todo

* spotless

---------

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
Co-authored-by: Ken Stevens <khstevens@gmail.com>
This commit is contained in:
jmarchionatto 2024-12-04 14:27:27 -05:00 committed by GitHub
parent 9d584d1b83
commit 9bfdbea1ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 276 additions and 0 deletions

View File

@ -97,8 +97,10 @@ import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl;
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.provider.DiffProvider; import ca.uhn.fhir.jpa.provider.DiffProvider;
import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc;
import ca.uhn.fhir.jpa.provider.InstanceReindexProvider; import ca.uhn.fhir.jpa.provider.InstanceReindexProvider;
import ca.uhn.fhir.jpa.provider.ProcessMessageProvider; import ca.uhn.fhir.jpa.provider.ProcessMessageProvider;
import ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider; import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
@ -926,4 +928,9 @@ public class JpaConfig {
ITagDefinitionDao tagDefinitionDao, MemoryCacheService memoryCacheService) { ITagDefinitionDao tagDefinitionDao, MemoryCacheService memoryCacheService) {
return new CacheTagDefinitionDao(tagDefinitionDao, memoryCacheService); return new CacheTagDefinitionDao(tagDefinitionDao, memoryCacheService);
} }
@Bean
public IReplaceReferencesSvc replaceReferencesSvc(FhirContext theFhirContext, DaoRegistry theDaoRegistry) {
return new ReplaceReferencesSvcImpl(theFhirContext, theDaoRegistry);
}
} }

View File

@ -69,6 +69,9 @@ public abstract class BaseJpaSystemProvider<T, MT> extends BaseStorageSystemProv
@Autowired @Autowired
private ITermReadSvc myTermReadSvc; private ITermReadSvc myTermReadSvc;
@Autowired
private IReplaceReferencesSvc myReplaceReferencesSvc;
public BaseJpaSystemProvider() { public BaseJpaSystemProvider() {
// nothing // nothing
} }
@ -77,6 +80,10 @@ public abstract class BaseJpaSystemProvider<T, MT> extends BaseStorageSystemProv
return myResourceReindexingSvc; return myResourceReindexingSvc;
} }
public IReplaceReferencesSvc getReplaceReferencesSvc() {
return myReplaceReferencesSvc;
}
@History @History
public IBundleProvider historyServer( public IBundleProvider historyServer(
HttpServletRequest theRequest, HttpServletRequest theRequest,

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseParameters;
/**
* Contract for service which replaces references
*/
public interface IReplaceReferencesSvc {
IBaseParameters replaceReferences(String theSourceRefId, String theTargetRefId, RequestDetails theRequest);
}

View File

@ -141,4 +141,17 @@ public final class JpaSystemProvider<T, MT> extends BaseJpaSystemProvider<T, MT>
endRequest(((ServletRequestDetails) theRequestDetails).getServletRequest()); endRequest(((ServletRequestDetails) theRequestDetails).getServletRequest());
} }
} }
@Operation(name = ProviderConstants.OPERATION_REPLACE_REFERENCES, global = true)
@Description(
value =
"This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.",
shortDefinition = "Repoints referencing resources to another resources instance")
public IBaseParameters replaceReferences(
@OperationParam(name = ProviderConstants.PARAM_SOURCE_REFERENCE_ID) String theSourceId,
@OperationParam(name = ProviderConstants.PARAM_TARGET_REFERENCE_ID) String theTargetId,
RequestDetails theRequest) {
return getReplaceReferencesSvc().replaceReferences(theSourceId, theTargetId, theRequest);
}
} }

View File

@ -0,0 +1,221 @@
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type;
import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE;
import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION;
import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH;
import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE;
import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE;
import static ca.uhn.fhir.rest.api.Constants.PARAM_ID;
import static ca.uhn.fhir.rest.server.provider.ProviderConstants.PARAM_SOURCE_REFERENCE_ID;
import static ca.uhn.fhir.rest.server.provider.ProviderConstants.PARAM_TARGET_REFERENCE_ID;
import static software.amazon.awssdk.utils.StringUtils.isBlank;
public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc {
private final FhirContext myFhirContext;
private final DaoRegistry myDaoRegistry;
public ReplaceReferencesSvcImpl(FhirContext theFhirContext, DaoRegistry theDaoRegistry) {
myFhirContext = theFhirContext;
myDaoRegistry = theDaoRegistry;
}
@Override
public IBaseParameters replaceReferences(String theSourceRefId, String theTargetRefId, RequestDetails theRequest) {
validateParameters(theSourceRefId, theTargetRefId);
IIdType sourceRefId = new IdDt(theSourceRefId);
IIdType targetRefId = new IdDt(theTargetRefId);
// todo jm: this could be problematic depending on referenceing object set size, however we are adding
// batch job option to handle that case as part of this feature
List<? extends IBaseResource> referencingResources = findReferencingResourceIds(sourceRefId, theRequest);
return replaceReferencesInTransaction(referencingResources, sourceRefId, targetRefId, theRequest);
}
private IBaseParameters replaceReferencesInTransaction(
List<? extends IBaseResource> theReferencingResources,
IIdType theCurrentTargetId,
IIdType theNewTargetId,
RequestDetails theRequest) {
Parameters resultParams = new Parameters();
// map resourceType -> map resourceId -> patch Parameters
Map<String, Map<IIdType, Parameters>> parametersMap =
buildPatchParameterMap(theReferencingResources, theCurrentTargetId, theNewTargetId);
for (Map.Entry<String, Map<IIdType, Parameters>> mapEntry : parametersMap.entrySet()) {
String resourceType = mapEntry.getKey();
IFhirResourceDao<?> resDao = myDaoRegistry.getResourceDao(resourceType);
if (resDao == null) {
throw new InternalErrorException(
Msg.code(2588) + "No DAO registered for resource type: " + resourceType);
}
// patch each resource of resourceType
patchResourceTypeResources(mapEntry, resDao, resultParams, theRequest);
}
return resultParams;
}
private void patchResourceTypeResources(
Map.Entry<String, Map<IIdType, Parameters>> mapEntry,
IFhirResourceDao<?> resDao,
Parameters resultParams,
RequestDetails theRequest) {
for (Map.Entry<IIdType, Parameters> idParamMapEntry :
mapEntry.getValue().entrySet()) {
IIdType resourceId = idParamMapEntry.getKey();
Parameters parameters = idParamMapEntry.getValue();
MethodOutcome result =
resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequest);
resultParams.addParameter().setResource((Resource) result.getOperationOutcome());
}
}
private Map<String, Map<IIdType, Parameters>> buildPatchParameterMap(
List<? extends IBaseResource> theReferencingResources,
IIdType theCurrentReferencedResourceId,
IIdType theNewReferencedResourceId) {
Map<String, Map<IIdType, Parameters>> paramsMap = new HashMap<>();
for (IBaseResource referencingResource : theReferencingResources) {
// resource can have more than one reference to the same target resource
for (ResourceReferenceInfo refInfo :
myFhirContext.newTerser().getAllResourceReferences(referencingResource)) {
addReferenceToMapIfForSource(
theCurrentReferencedResourceId,
theNewReferencedResourceId,
referencingResource,
refInfo,
paramsMap);
}
}
return paramsMap;
}
private void addReferenceToMapIfForSource(
IIdType theCurrentReferencedResourceId,
IIdType theNewReferencedResourceId,
IBaseResource referencingResource,
ResourceReferenceInfo refInfo,
Map<String, Map<IIdType, Parameters>> paramsMap) {
if (!refInfo.getResourceReference()
.getReferenceElement()
.toUnqualifiedVersionless()
.getValueAsString()
.equals(theCurrentReferencedResourceId
.toUnqualifiedVersionless()
.getValueAsString())) {
// not a reference to the resource being replaced
return;
}
Parameters.ParametersParameterComponent paramComponent = createReplaceReferencePatchOperation(
referencingResource.fhirType() + "." + refInfo.getName(),
new Reference(
theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString()));
paramsMap
// preserve order, in case it could matter
.computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>())
.computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters())
.addParameter(paramComponent);
}
@Nonnull
private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation(
String thePath, Type theValue) {
Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent();
operation.setName(PARAMETER_OPERATION);
operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE));
operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath));
operation.addPart().setName(PARAMETER_VALUE).setValue(theValue);
return operation;
}
private List<? extends IBaseResource> findReferencingResourceIds(
IIdType theSourceRefIdParam, RequestDetails theRequest) {
IFhirResourceDao<?> dao = getDao(theSourceRefIdParam.getResourceType());
if (dao == null) {
throw new InternalErrorException(
Msg.code(2582) + "Couldn't obtain DAO for resource type" + theSourceRefIdParam.getResourceType());
}
SearchParameterMap parameterMap = new SearchParameterMap();
parameterMap.add(PARAM_ID, new StringParam(theSourceRefIdParam.getValue()));
parameterMap.addRevInclude(new Include("*"));
return dao.search(parameterMap, theRequest).getAllResources();
}
private IFhirResourceDao<?> getDao(String theResourceName) {
return myDaoRegistry.getResourceDao(theResourceName);
}
private void validateParameters(String theSourceRefIdParam, String theTargetRefIdParam) {
if (isBlank(theSourceRefIdParam)) {
throw new InvalidParameterException(
Msg.code(2583) + "Parameter '" + PARAM_SOURCE_REFERENCE_ID + "' is blank");
}
if (isBlank(theTargetRefIdParam)) {
throw new InvalidParameterException(
Msg.code(2584) + "Parameter '" + PARAM_TARGET_REFERENCE_ID + "' is blank");
}
IIdType sourceId = new IdDt(theSourceRefIdParam);
if (isBlank(sourceId.getResourceType())) {
throw new InvalidParameterException(
Msg.code(2585) + "'" + PARAM_SOURCE_REFERENCE_ID + "' must be a resource type qualified id");
}
IIdType targetId = new IdDt(theTargetRefIdParam);
if (isBlank(targetId.getResourceType())) {
throw new InvalidParameterException(
Msg.code(2586) + "'" + PARAM_TARGET_REFERENCE_ID + "' must be a resource type qualified id");
}
if (!targetId.getResourceType().equals(sourceId.getResourceType())) {
throw new InvalidParameterException(
Msg.code(2587) + "Source and target id parameters must be for the same resource type");
}
}
}

View File

@ -239,8 +239,24 @@ public class ProviderConstants {
* Operation name for the "$export-poll-status" operation * Operation name for the "$export-poll-status" operation
*/ */
public static final String OPERATION_EXPORT_POLL_STATUS = "$export-poll-status"; public static final String OPERATION_EXPORT_POLL_STATUS = "$export-poll-status";
/** /**
* Operation name for the "$export" operation * Operation name for the "$export" operation
*/ */
public static final String OPERATION_EXPORT = "$export"; public static final String OPERATION_EXPORT = "$export";
/**
* Operation name for the "$replace-references" operation
*/
public static final String OPERATION_REPLACE_REFERENCES = "$replace-references";
/**
* Parameter for source reference of the "$replace-references" operation
*/
public static final String PARAM_SOURCE_REFERENCE_ID = "sourceReferenceId";
/**
* Parameter for target reference of the "$replace-references" operation
*/
public static final String PARAM_TARGET_REFERENCE_ID = "targetReferenceId";
} }