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:
parent
9d584d1b83
commit
9bfdbea1ef
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue