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.RequestPartitionHelperSvc;
|
||||
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.ProcessMessageProvider;
|
||||
import ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl;
|
||||
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
|
||||
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
||||
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
|
||||
|
@ -926,4 +928,9 @@ public class JpaConfig {
|
|||
ITagDefinitionDao tagDefinitionDao, MemoryCacheService 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
|
||||
private ITermReadSvc myTermReadSvc;
|
||||
|
||||
@Autowired
|
||||
private IReplaceReferencesSvc myReplaceReferencesSvc;
|
||||
|
||||
public BaseJpaSystemProvider() {
|
||||
// nothing
|
||||
}
|
||||
|
@ -77,6 +80,10 @@ public abstract class BaseJpaSystemProvider<T, MT> extends BaseStorageSystemProv
|
|||
return myResourceReindexingSvc;
|
||||
}
|
||||
|
||||
public IReplaceReferencesSvc getReplaceReferencesSvc() {
|
||||
return myReplaceReferencesSvc;
|
||||
}
|
||||
|
||||
@History
|
||||
public IBundleProvider historyServer(
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
*/
|
||||
public static final String OPERATION_EXPORT_POLL_STATUS = "$export-poll-status";
|
||||
|
||||
/**
|
||||
* Operation name for the "$export" operation
|
||||
*/
|
||||
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