New extension for auto versioning references of the resource (#5591)

New extension for auto-versioning references of the resource
This commit is contained in:
volodymyr-korzh 2024-01-12 17:00:58 -07:00 committed by GitHub
parent 763894c28f
commit 5286829585
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 665 additions and 481 deletions

View File

@ -43,6 +43,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.MetaUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
import jakarta.annotation.Nullable;
@ -217,7 +218,8 @@ public abstract class BaseParser implements IParser {
});
}
private String determineReferenceText(IBaseReference theRef, CompositeChildElement theCompositeChildElement) {
private String determineReferenceText(
IBaseReference theRef, CompositeChildElement theCompositeChildElement, IBaseResource theResource) {
IIdType ref = theRef.getReferenceElement();
if (isBlank(ref.getIdPart())) {
String reference = ref.getValue();
@ -241,7 +243,7 @@ public abstract class BaseParser implements IParser {
.getResourceDefinition(theRef.getResource())
.getName());
}
if (isStripVersionsFromReferences(theCompositeChildElement)) {
if (isStripVersionsFromReferences(theCompositeChildElement, theResource)) {
reference = refId.toVersionless().getValue();
} else {
reference = refId.getValue();
@ -258,12 +260,12 @@ public abstract class BaseParser implements IParser {
myContext.getResourceDefinition(theRef.getResource()).getName());
}
if (isNotBlank(myServerBaseUrl) && StringUtils.equals(myServerBaseUrl, ref.getBaseUrl())) {
if (isStripVersionsFromReferences(theCompositeChildElement)) {
if (isStripVersionsFromReferences(theCompositeChildElement, theResource)) {
return ref.toUnqualifiedVersionless().getValue();
}
return ref.toUnqualified().getValue();
}
if (isStripVersionsFromReferences(theCompositeChildElement)) {
if (isStripVersionsFromReferences(theCompositeChildElement, theResource)) {
return ref.toVersionless().getValue();
}
return ref.getValue();
@ -604,7 +606,17 @@ public abstract class BaseParser implements IParser {
return myContext.getParserOptions().isOverrideResourceIdWithBundleEntryFullUrl();
}
private boolean isStripVersionsFromReferences(CompositeChildElement theCompositeChildElement) {
private boolean isStripVersionsFromReferences(
CompositeChildElement theCompositeChildElement, IBaseResource theResource) {
Set<String> autoVersionReferencesAtPathExtensions =
MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), myContext.getResourceType(theResource));
if (!autoVersionReferencesAtPathExtensions.isEmpty()
&& theCompositeChildElement.anyPathMatches(autoVersionReferencesAtPathExtensions)) {
return false;
}
Boolean stripVersionsFromReferences = myStripVersionsFromReferences;
if (stripVersionsFromReferences != null) {
return stripVersionsFromReferences;
@ -811,7 +823,7 @@ public abstract class BaseParser implements IParser {
*/
if (next instanceof IBaseReference) {
IBaseReference nextRef = (IBaseReference) next;
String refText = determineReferenceText(nextRef, theCompositeChildElement);
String refText = determineReferenceText(nextRef, theCompositeChildElement, theResource);
if (!StringUtils.equals(refText, nextRef.getReferenceElement().getValue())) {
if (retVal == theValues) {

View File

@ -30,6 +30,7 @@ import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
@ -177,15 +178,18 @@ public class ExtensionUtil {
* pulls out any extensions that have the given theExtensionUrl and a primitive value type,
* and returns a list of the string version of the extension values.
*/
public static List<String> getExtensionPrimitiveValues(IBaseHasExtensions theBase, String theExtensionUrl) {
List<String> values = theBase.getExtension().stream()
.filter(t -> theExtensionUrl.equals(t.getUrl()))
.filter(t -> t.getValue() instanceof IPrimitiveType<?>)
.map(t -> (IPrimitiveType<?>) t.getValue())
.map(IPrimitiveType::getValueAsString)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
return values;
public static List<String> getExtensionPrimitiveValues(IBase theBase, String theExtensionUrl) {
if (theBase instanceof IBaseHasExtensions) {
return ((IBaseHasExtensions) theBase)
.getExtension().stream()
.filter(t -> theExtensionUrl.equals(t.getUrl()))
.filter(t -> t.getValue() instanceof IPrimitiveType<?>)
.map(t -> (IPrimitiveType<?>) t.getValue())
.map(IPrimitiveType::getValueAsString)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
/**

View File

@ -162,6 +162,16 @@ public class HapiExtensions {
*/
public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN =
"https://smilecdr.com/fhir/ns/StructureDefinition/searchparameter-uplift-refchain";
/**
* This extension is used to enable auto version references at path for resource instances.
* This extension should be of type <code>string</code> and should be
* placed on the <code>Resource.meta</code> element.
* It is allowed to add multiple extensions with different paths.
*/
public static final String EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH =
"http://hapifhir.io/fhir/StructureDefinition/auto-version-references-at-path";
/**
* This extension is used for "uplifted refchains" on search parameters. See the
* HAPI FHIR documentation for an explanation of how these work.

View File

@ -35,6 +35,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -144,4 +146,12 @@ public class MetaUtil {
}
sourceElement.setValueAsString(theValue);
}
public static Set<String> getAutoVersionReferencesAtPath(IBaseMetaType theMeta, String theResourceType) {
return ExtensionUtil.getExtensionPrimitiveValues(
theMeta, HapiExtensions.EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH)
.stream()
.map(path -> String.format("%s.%s", theResourceType, path))
.collect(Collectors.toSet());
}
}

View File

@ -0,0 +1,5 @@
---
type: add
issue: 5588
title: "Added `auto-version-references-at-path` extension that allows to
enable auto versioning references at specified paths of resource instances."

View File

@ -166,3 +166,22 @@ You can also configure HAPI to not strip versions only on certain fields. This i
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/Parser.java|disableStripVersionsField}}
```
# Automatically Versioned References
It is possible to configure HAPI to automatically version references for desired resource instances by providing the `auto-version-references-at-path` extension in the `Resource.meta` element:
```json
"meta": {
"extension":[
{
"url":"http://hapifhir.io/fhir/StructureDefinition/auto-version-references-at-path",
"valueString":"focus"
}
]
}
```
It is allowed to add multiple extensions with different paths. When a resource is stored, any references found at the specified paths will have the current version of the target appended, if a version is not already present.
Parser will not strip versions from references at paths provided by the `auto-version-references-at-path` extension.

View File

@ -157,6 +157,7 @@ import org.hl7.fhir.r4.model.Media;
import org.hl7.fhir.r4.model.Medication;
import org.hl7.fhir.r4.model.MedicationAdministration;
import org.hl7.fhir.r4.model.MedicationRequest;
import org.hl7.fhir.r4.model.MessageHeader;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.MolecularSequence;
import org.hl7.fhir.r4.model.NamingSystem;
@ -433,6 +434,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
@Qualifier("myExplanationOfBenefitDaoR4")
protected IFhirResourceDao<ExplanationOfBenefit> myExplanationOfBenefitDao;
@Autowired
@Qualifier("myMessageHeaderDaoR4")
protected IFhirResourceDao<MessageHeader> myMessageHeaderDao;
@Autowired
protected IResourceTableDao myResourceTableDao;
@Autowired
protected IResourceHistoryTableDao myResourceHistoryTableDao;

View File

@ -59,7 +59,9 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.IMetaTagSorter;
import ca.uhn.fhir.util.MetaUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch;
@ -86,6 +88,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -690,29 +694,67 @@ public abstract class BaseStorageDao {
}
/**
* @see StorageSettings#getAutoVersionReferenceAtPaths()
* Extracts a list of references that should be auto-versioned.
*
* @return A set of references that should be versioned according to both storage settings
* and auto-version reference extensions, or it may also be empty.
*/
@Nonnull
public static Set<IBaseReference> extractReferencesToAutoVersion(
FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) {
Map<IBaseReference, Object> references = Collections.emptyMap();
Set<IBaseReference> referencesToAutoVersionFromConfig =
getReferencesToAutoVersionFromConfig(theFhirContext, theStorageSettings, theResource);
Set<IBaseReference> referencesToAutoVersionFromExtensions =
getReferencesToAutoVersionFromExtension(theFhirContext, theResource);
return Stream.concat(referencesToAutoVersionFromConfig.stream(), referencesToAutoVersionFromExtensions.stream())
.collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new))
.keySet();
}
/**
* Extracts a list of references that should be auto-versioned according to
* <code>auto-version-references-at-path</code> extensions.
* @see HapiExtensions#EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH
*/
@Nonnull
private static Set<IBaseReference> getReferencesToAutoVersionFromExtension(
FhirContext theFhirContext, IBaseResource theResource) {
String resourceType = theFhirContext.getResourceType(theResource);
Set<String> autoVersionReferencesAtPaths =
MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType);
if (!autoVersionReferencesAtPaths.isEmpty()) {
return getReferencesWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource);
}
return Collections.emptySet();
}
/**
* Extracts a list of references that should be auto-versioned according to storage configuration.
* @see StorageSettings#getAutoVersionReferenceAtPaths()
*/
@Nonnull
private static Set<IBaseReference> getReferencesToAutoVersionFromConfig(
FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) {
if (!theStorageSettings.getAutoVersionReferenceAtPaths().isEmpty()) {
String resourceName = theFhirContext.getResourceType(theResource);
for (String nextPath : theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName)) {
List<IBaseReference> nextReferences =
theFhirContext.newTerser().getValues(theResource, nextPath, IBaseReference.class);
for (IBaseReference next : nextReferences) {
if (next.getReferenceElement().hasVersionIdPart()) {
continue;
}
if (references.isEmpty()) {
references = new IdentityHashMap<>();
}
references.put(next, null);
}
}
Set<String> autoVersionReferencesPaths =
theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName);
return getReferencesWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource);
}
return references.keySet();
return Collections.emptySet();
}
private static Set<IBaseReference> getReferencesWithoutVersionId(
Set<String> autoVersionReferencesPaths, FhirContext theFhirContext, IBaseResource theResource) {
return autoVersionReferencesPaths.stream()
.map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class))
.flatMap(Collection::stream)
.filter(reference -> !reference.getReferenceElement().hasVersionIdPart())
.collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new))
.keySet();
}
public static void clearRequestAsProcessingSubRequest(RequestDetails theRequestDetails) {

View File

@ -58,6 +58,7 @@ import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
@ -1680,13 +1681,10 @@ public abstract class BaseTransactionProcessor {
if (newId != null) {
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
addRollbackReferenceRestore(theTransactionDetails, resourceReference);
if (theReferencesToAutoVersion.contains(resourceReference)) {
resourceReference.setReference(newId.getValue());
resourceReference.setResource(null);
replaceResourceReference(newId, resourceReference, theTransactionDetails);
} else {
resourceReference.setReference(newId.toVersionless().getValue());
resourceReference.setResource(null);
replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails);
}
}
} else if (nextId.getValue().startsWith("urn:")) {
@ -1724,9 +1722,15 @@ public abstract class BaseTransactionProcessor {
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId);
if (outcome != null && !outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
addRollbackReferenceRestore(theTransactionDetails, resourceReference);
resourceReference.setReference(nextId.getValue());
resourceReference.setResource(null);
replaceResourceReference(nextId, resourceReference, theTransactionDetails);
}
// if referenced resource is not in transaction but exists in the DB, resolving its version
IResourcePersistentId persistedReferenceId = resourceVersionMap.getResourcePersistentId(nextId);
if (outcome == null && persistedReferenceId != null && persistedReferenceId.getVersion() != null) {
IIdType newReferenceId = nextId.withVersion(
persistedReferenceId.getVersion().toString());
replaceResourceReference(newReferenceId, resourceReference, theTransactionDetails);
}
}
}
@ -1814,6 +1818,13 @@ public abstract class BaseTransactionProcessor {
}
}
private void replaceResourceReference(
IIdType theReferenceId, IBaseReference theResourceReference, TransactionDetails theTransactionDetails) {
addRollbackReferenceRestore(theTransactionDetails, theResourceReference);
theResourceReference.setReference(theReferenceId.getValue());
theResourceReference.setResource(null);
}
private void addRollbackReferenceRestore(
TransactionDetails theTransactionDetails, IBaseReference resourceReference) {
String existingValue = resourceReference.getReferenceElement().getValue();