3658 adding updating with history rewrite feature (#3659)

* Updating With History Rewrite implementation

* Missed new files

* Updated changelog and documentation

* Fixed test failures and doc

* minor fix

* lgtm code refactor

* Fixed test to clear DaoConfig

* Removed unused class

* Code review changes

* Simplified updateHistoryRewrite's fluent, combining with normal update's, also changed logic on custom header error checking, updated tests

* refactored method to extracted common code

* removed `updateHistoryRewrite()` and replaced with `update()`, updated tests accordingly

Co-authored-by: Steven Li <steven@smilecdr.com>
This commit is contained in:
StevenXLi 2022-06-15 12:40:58 -04:00 committed by GitHub
parent bf2f126c91
commit 3da39aeda3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1001 additions and 103 deletions

View File

@ -25,7 +25,7 @@ public final class Msg {
/**
* IMPORTANT: Please update the following comment after you add a new code
* Last code value: 2086
* Last code value: 2094
*/
private Msg() {}

View File

@ -22,7 +22,12 @@ package ca.uhn.fhir.rest.api;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
@ -139,6 +144,7 @@ public class Constants {
public static final String HEADER_PREFER_RETURN_OPERATION_OUTCOME = "OperationOutcome";
public static final String HEADER_SUFFIX_CT_UTF_8 = "; charset=UTF-8";
public static final String HEADERVALUE_CORS_ALLOW_METHODS_ALL = "GET, POST, PUT, DELETE, OPTIONS";
public static final String HEADER_REWRITE_HISTORY = "X-Rewrite-History";
public static final Map<Integer, String> HTTP_STATUS_NAMES;
public static final String LINK_FHIR_BASE = "fhir-base";
public static final String LINK_FIRST = "first";

View File

@ -21,13 +21,12 @@ package ca.uhn.fhir.rest.api;
* #L%
*/
import java.util.HashMap;
import java.util.Map;
import ca.uhn.fhir.util.CoverageIgnore;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
@CoverageIgnore
public enum RestOperationTypeEnum {
@ -144,12 +143,16 @@ public enum RestOperationTypeEnum {
* $meta-delete extended operation
*/
META_DELETE("$meta-delete", false, false, false),
/**
* Patch operation
*/
PATCH("patch", false, false, true),
/**
* Code Value: <b>update-rewrite-history</b>
*/
UPDATE_REWRITE_HISTORY("update-rewrite-history", false, false, true),
;
private static final Map<String, RestOperationTypeEnum> CODE_TO_ENUM = new HashMap<String, RestOperationTypeEnum>();

View File

@ -20,15 +20,26 @@ package ca.uhn.fhir.rest.client.api;
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
import ca.uhn.fhir.rest.client.exceptions.FhirClientInappropriateForServerException;
import ca.uhn.fhir.rest.gclient.*;
import ca.uhn.fhir.rest.gclient.ICreate;
import ca.uhn.fhir.rest.gclient.IDelete;
import ca.uhn.fhir.rest.gclient.IFetchConformanceUntyped;
import ca.uhn.fhir.rest.gclient.IGetPage;
import ca.uhn.fhir.rest.gclient.IHistory;
import ca.uhn.fhir.rest.gclient.IMeta;
import ca.uhn.fhir.rest.gclient.IOperation;
import ca.uhn.fhir.rest.gclient.IPatch;
import ca.uhn.fhir.rest.gclient.IRead;
import ca.uhn.fhir.rest.gclient.ITransaction;
import ca.uhn.fhir.rest.gclient.IUntypedQuery;
import ca.uhn.fhir.rest.gclient.IUpdate;
import ca.uhn.fhir.rest.gclient.IValidate;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IGenericClient extends IRestfulClient {
@ -193,7 +204,7 @@ public interface IGenericClient extends IRestfulClient {
/**
* Implementation of the "instance update" method.
*
*
* @param theId
* The ID to update
* @param theResource
@ -211,7 +222,7 @@ public interface IGenericClient extends IRestfulClient {
/**
* Implementation of the "type validate" method.
*
*
* @param theResource
* The resource to validate
* @return An outcome containing any validation issues

View File

@ -43,4 +43,5 @@ public interface IUpdateTyped extends IUpdateExecutable {
*/
IUpdateWithQuery conditional();
IUpdateTyped historyRewrite();
}

View File

@ -20,7 +20,6 @@ package ca.uhn.fhir.rest.client.impl;
* #L%
*/
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
@ -28,6 +27,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.IRuntimeDatatypeDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
@ -2245,6 +2245,13 @@ public class GenericClient extends BaseClient implements IGenericClient {
private IBaseResource myResource;
private String myResourceBody;
private String mySearchUrl;
private boolean myIsHistoryRewrite;
@Override
public IUpdateTyped historyRewrite() {
myIsHistoryRewrite = true;
return this;
}
@Override
public IUpdateWithQuery conditional() {
@ -2282,7 +2289,16 @@ public class GenericClient extends BaseClient implements IGenericClient {
if (myId == null || myId.hasIdPart() == false) {
throw new InvalidRequestException(Msg.code(1396) + "No ID supplied for resource to update, can not invoke server");
}
invocation = MethodUtil.createUpdateInvocation(myResource, myResourceBody, myId, myContext);
if (myIsHistoryRewrite) {
if (!myId.hasVersionIdPart()) {
throw new InvalidRequestException(Msg.code(2090) + "ID must contain a history version, found: " + myId.getVersionIdPart());
}
invocation = MethodUtil.createUpdateHistoryRewriteInvocation(myResource, myResourceBody, myId, myContext);
invocation.addHeader(Constants.HEADER_REWRITE_HISTORY, "true");
} else {
invocation = MethodUtil.createUpdateInvocation(myResource, myResourceBody, myId, myContext);
}
}
addPreferHeader(myPrefer, invocation);

View File

@ -1,10 +1,10 @@
package ca.uhn.fhir.rest.client.method;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
@ -12,7 +12,23 @@ import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.annotation.At;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Count;
import ca.uhn.fhir.rest.annotation.Elements;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.Offset;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RawParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.annotation.Sort;
import ca.uhn.fhir.rest.annotation.TransactionParam;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
@ -188,7 +204,7 @@ public class MethodUtil {
}
public static HttpPutClientInvocation createUpdateInvocation(IBaseResource theResource, String theResourceBody,
IIdType theId, FhirContext theContext) {
IIdType theId, FhirContext theContext) {
String resourceName = theContext.getResourceType(theResource);
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(resourceName);
@ -212,6 +228,32 @@ public class MethodUtil {
return retVal;
}
public static HttpPutClientInvocation createUpdateHistoryRewriteInvocation(IBaseResource theResource, String theResourceBody,
IIdType theId, FhirContext theContext) {
String resourceName = theContext.getResourceType(theResource);
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(resourceName);
urlBuilder.append('/');
urlBuilder.append(theId.getIdPart());
if (theId.hasVersionIdPart()) {
urlBuilder.append('/');
urlBuilder.append(Constants.PARAM_HISTORY);
urlBuilder.append('/');
urlBuilder.append(theId.getVersionIdPart());
}
String urlExtension = urlBuilder.toString();
HttpPutClientInvocation retVal;
if (StringUtils.isBlank(theResourceBody)) {
retVal = new HttpPutClientInvocation(theContext, theResource, urlExtension);
} else {
retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, urlExtension);
}
return retVal;
}
public static StringBuilder createUrl(String theResourceType, Map<String, List<String>> theMatchParams) {
StringBuilder b = new StringBuilder();

View File

@ -0,0 +1,7 @@
---
type: add
issue: 3658
jira: SMILE-3080, SMILE-3372
title: "Currently neither HAPI-FHIR nor the FHIR specification supports modifying historical version of a resource. This
implementation adds the feature Update with History Rewrite to HAPI-FHIR. It can be accessed via the IGenericClient's
`updateHistoryRewrite` method"

View File

@ -124,17 +124,51 @@ Content-Type: application/fhir+json
{ ..resource body.. }
```
If a client performs a contention aware update, the ETag version will be placed in the version part of the IdDt/IdType that is passed into the method. For example:
If a client performs a contention aware update, the ETag version will be placed in the version part of the IdDt/IdType
that is passed into the method. For example:
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java|updateEtag}}
```
## Update with History Rewrite
If you wish to update a historical version of a resource without creating a new version, this can now be done with the
Update operation. While this operation is not supported by the FHIR specification, it's an enhancement added to
specifically to HAPI-FHIR.
In order to use this new functionality, you must set the `setUpdateWithHistoryRewriteEnabled` setting in the `DaoConfig`
to true.
The following API request shows an example of executing a PUT at the following endpoint.
The request must include the header `X-Rewrite-History`, and should be set to true. The body of the request must include
the resource with the same ID and version as defined in the PUT request,
```http
PUT [serverBase]/Patient/123/_history/3
Content-Type: application/fhir+json
X-Rewrite-History: true
{
..
id: "123",
meta: {
versionId: "3",
..
}
..
}
```
<a name="instance_delete" />
# Instance Level - Delete
The [delete](http://hl7.org/implement/standards/fhir/http.html#delete) operation retrieves a specific version of a resource with a given ID. It takes a single ID parameter annotated with an [@IdParam](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/annotation/IdParam.html) annotation, which supplies the ID of the resource to delete.
The [delete](http://hl7.org/implement/standards/fhir/http.html#delete) operation retrieves a specific version of a
resource with a given ID. It takes a single ID parameter annotated with
an [@IdParam](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/annotation/IdParam.html) annotation, which supplies the
ID of the resource to delete.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java|delete}}

View File

@ -156,6 +156,7 @@ import java.util.StringTokenizer;
import java.util.UUID;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -163,8 +164,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.left;
import static org.apache.commons.lang3.StringUtils.trim;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME;
/*
* #%L
* HAPI FHIR JPA Server
@ -592,53 +591,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
String resourceType = theEntity.getResourceType();
List<String> excludeElements = new ArrayList<>(8);
excludeElements.add("id");
IBaseMetaType meta = theResource.getMeta();
boolean hasExtensions = false;
IBaseExtension<?, ?> sourceExtension = null;
if (meta instanceof IBaseHasExtensions) {
List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) meta).getExtension();
if (!extensions.isEmpty()) {
hasExtensions = true;
/*
* FHIR DSTU3 did not have the Resource.meta.source field, so we use a
* custom HAPI FHIR extension in Resource.meta to store that field. However,
* we put the value for that field in a separate table so we don't want to serialize
* it into the stored BLOB. Therefore: remove it from the resource temporarily
* and restore it afterward.
*/
if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
for (int i = 0; i < extensions.size(); i++) {
if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) {
sourceExtension = extensions.remove(i);
i--;
}
}
}
}
}
boolean inlineTagMode = getConfig().getTagStorageMode() == DaoConfig.TagStorageModeEnum.INLINE;
if (hasExtensions || inlineTagMode) {
if (!inlineTagMode) {
excludeElements.add(resourceType + ".meta.profile");
excludeElements.add(resourceType + ".meta.tag");
excludeElements.add(resourceType + ".meta.security");
}
excludeElements.add(resourceType + ".meta.versionId");
excludeElements.add(resourceType + ".meta.lastUpdated");
excludeElements.add(resourceType + ".meta.source");
} else {
/*
* If there are no extensions in the meta element, we can just exclude the
* whole meta element, which avoids adding an empty "meta":{}
* from showing up in the serialized JSON.
*/
excludeElements.add(resourceType + ".meta");
}
IBaseExtension<?, ?> sourceExtension = getExcludedElements(resourceType, excludeElements, meta);
theEntity.setFhirVersion(myContext.getVersion().getVersion());
@ -652,18 +607,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
hashCode = sha256.hashUnencodedChars(encodedResource);
} else {
resourceText = null;
switch (encoding) {
case JSON:
resourceBinary = encodedResource.getBytes(Charsets.UTF_8);
break;
case JSONC:
resourceBinary = GZipUtil.compress(encodedResource);
break;
default:
case DEL:
resourceBinary = new byte[0];
break;
}
resourceBinary = getResourceBinary(encoding, encodedResource);
hashCode = sha256.hashBytes(resourceBinary);
}
@ -737,6 +681,88 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
return retVal;
}
/**
* helper for returning the encoded byte array of the input resource string based on the encoding.
*
* @param encoding the encoding to used
* @param encodedResource the resource to encode
* @return byte array of the resource
*/
@Nonnull
private byte[] getResourceBinary(ResourceEncodingEnum encoding, String encodedResource) {
byte[] resourceBinary;
switch (encoding) {
case JSON:
resourceBinary = encodedResource.getBytes(Charsets.UTF_8);
break;
case JSONC:
resourceBinary = GZipUtil.compress(encodedResource);
break;
default:
case DEL:
resourceBinary = new byte[0];
break;
}
return resourceBinary;
}
/**
* helper to format the meta element for serialization of the resource.
*
* @param theResourceType the resource type of the resource
* @param theExcludeElements list of extensions in the meta element to exclude from serialization
* @param theMeta the meta element of the resource
* @return source extension if present in the meta element
*/
private IBaseExtension<?, ?> getExcludedElements(String theResourceType, List<String> theExcludeElements, IBaseMetaType theMeta) {
boolean hasExtensions = false;
IBaseExtension<?, ?> sourceExtension = null;
if (theMeta instanceof IBaseHasExtensions) {
List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) theMeta).getExtension();
if (!extensions.isEmpty()) {
hasExtensions = true;
/*
* FHIR DSTU3 did not have the Resource.meta.source field, so we use a
* custom HAPI FHIR extension in Resource.meta to store that field. However,
* we put the value for that field in a separate table, so we don't want to serialize
* it into the stored BLOB. Therefore: remove it from the resource temporarily
* and restore it afterward.
*/
if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
for (int i = 0; i < extensions.size(); i++) {
if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) {
sourceExtension = extensions.remove(i);
i--;
}
}
}
}
}
theExcludeElements.add("id");
boolean inlineTagMode = getConfig().getTagStorageMode() == DaoConfig.TagStorageModeEnum.INLINE;
if (hasExtensions || inlineTagMode) {
if (!inlineTagMode) {
theExcludeElements.add(theResourceType + ".meta.profile");
theExcludeElements.add(theResourceType + ".meta.tag");
theExcludeElements.add(theResourceType + ".meta.security");
}
theExcludeElements.add(theResourceType + ".meta.versionId");
theExcludeElements.add(theResourceType + ".meta.lastUpdated");
theExcludeElements.add(theResourceType + ".meta.source");
} else {
/*
* If there are no extensions in the meta element, we can just exclude the
* whole meta element, which avoids adding an empty "meta":{}
* from showing up in the serialized JSON.
*/
theExcludeElements.add(theResourceType + ".meta");
}
return sourceExtension;
}
private boolean updateTags(TransactionDetails theTransactionDetails, RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity) {
Set<ResourceTag> allDefs = new HashSet<>();
Set<ResourceTag> allTagsOld = getAllTagDefinitions(theEntity);
@ -1461,6 +1487,94 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
return entity;
}
public IBasePersistedResource updateHistoryEntity(RequestDetails theRequest, T theResource, IBasePersistedResource
theEntity, IBasePersistedResource theHistoryEntity, IIdType theResourceId, TransactionDetails theTransactionDetails, boolean isUpdatingCurrent) {
Validate.notNull(theEntity);
Validate.isTrue(theResource != null, "Must have either a resource[%s] for resource PID[%s]", theResource != null, theEntity.getPersistentId());
ourLog.debug("Starting history entity update");
EncodedResource encodedResource = new EncodedResource();
ResourceHistoryTable historyEntity;
if (isUpdatingCurrent) {
ResourceTable entity = (ResourceTable) theEntity;
IBaseResource oldResource;
if (getConfig().isMassIngestionMode()) {
oldResource = null;
} else {
oldResource = toResource(entity, false);
}
if (theRequest.getServer() != null) {
ActionRequestDetails actionRequestDetails = new ActionRequestDetails(theRequest, theResource, theResourceId.getResourceType(), theResourceId);
}
notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true);
ResourceTable savedEntity = updateEntity(theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false);
// Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version constraint failure, ie updating the same resource at the same time
encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
// For some reason the current version entity is not attached until after using updateEntity
historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity();
// Update version/lastUpdated so that interceptors see the correct version
updateResourceMetadata(savedEntity, theResource);
// Populate the PID in the resource, so it is available to hooks
addPidToResource(savedEntity, theResource);
if (!savedEntity.isUnchangedInCurrentOperation()) {
notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false);
}
} else {
historyEntity = (ResourceHistoryTable) theHistoryEntity;
if (!StringUtils.isBlank(historyEntity.getResourceType())) {
validateIncomingResourceTypeMatchesExisting(theResource, historyEntity);
}
historyEntity.setDeleted(null);
// Check if resource is the same
ResourceEncodingEnum encoding = myConfig.getResourceEncoding();
List<String> excludeElements = new ArrayList<>(8);
getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta());
String encodedResourceString = encodeResource(theResource, encoding, excludeElements, myContext);
byte[] resourceBinary = getResourceBinary(encoding, encodedResourceString);
boolean changed = !Arrays.equals(historyEntity.getResource(), resourceBinary);
historyEntity.setUpdated(theTransactionDetails.getTransactionDate());
if (!changed && myConfig.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) {
ourLog.debug("Resource {} has not changed", historyEntity.getIdDt().toUnqualified().getValue());
updateResourceMetadata(historyEntity, theResource);
return historyEntity;
}
if (getConfig().getInlineResourceTextBelowSize() > 0 && encodedResourceString.length() < getConfig().getInlineResourceTextBelowSize()) {
populateEncodedResource(encodedResource, encodedResourceString, null, ResourceEncodingEnum.JSON);
} else {
populateEncodedResource(encodedResource, null, resourceBinary, encoding);
}
}
/*
* Save the resource itself to the resourceHistoryTable
*/
historyEntity = myEntityManager.merge(historyEntity);
historyEntity.setEncoding(encodedResource.getEncoding());
historyEntity.setResource(encodedResource.getResourceBinary());
historyEntity.setResourceTextVc(encodedResource.getResourceText());
myResourceHistoryTableDao.save(historyEntity);
updateResourceMetadata(historyEntity, theResource);
return historyEntity;
}
private void populateEncodedResource(EncodedResource encodedResource, String encodedResourceString, byte[] theResourceBinary, ResourceEncodingEnum theEncoding) {
encodedResource.setResourceText(encodedResourceString);
encodedResource.setResourceBinary(theResourceBinary);
encodedResource.setEncoding(theEncoding);
}
@NotNull
private Map<String, Boolean> getSearchParamPresenceMap(ResourceTable entity, ResourceIndexedSearchParams newParams) {
Map<String, Boolean> retval = new HashMap<>();
@ -1544,7 +1658,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
}
}
private void validateIncomingResourceTypeMatchesExisting(IBaseResource theResource, ResourceTable entity) {
private void validateIncomingResourceTypeMatchesExisting(IBaseResource theResource, BaseHasResource entity) {
String resourceType = myContext.getResourceType(theResource);
if (!resourceType.equals(entity.getResourceType())) {
throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + entity.getResourceType() + "] - Cannot update with [" + resourceType + "]");
@ -1569,13 +1683,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
}
// Notify IServerOperationInterceptors about pre-action call
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, theTransactionDetails);
doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, hookParams);
notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true);
// Perform update
ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, theForceUpdateVersion, thePerformIndexing);
@ -1600,19 +1708,30 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
// Notify interceptors
if (!savedEntity.isUnchangedInCurrentOperation()) {
hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, theTransactionDetails)
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false);
}
return savedEntity;
}
private void notifyInterceptors(RequestDetails theRequestDetails, T theResource, IBaseResource theOldResource, TransactionDetails theTransactionDetails, boolean isUnchanged) {
Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED;
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, theTransactionDetails);
if (!isUnchanged) {
hookParams.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED;
}
doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams);
}
protected void addPidToResource(IBasePersistedResource theEntity, IBaseResource theResource) {
if (theResource instanceof IAnyResource) {
IDao.RESOURCE_PID.put((IAnyResource) theResource, theEntity.getPersistentId().getIdAsLong());

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.delete.DeleteConflictUtil;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.BaseTag;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
@ -70,7 +71,6 @@ import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
@ -1712,7 +1712,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
Runnable onRollback = () -> theResource.getIdElement().setValue(id);
// Execute the update in a retryable transaction
return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
if (myDaoConfig.isUpdateWithHistoryRewriteEnabled() && "true".equals(theRequest.getHeader(JpaConstants.HEADER_REWRITE_HISTORY))) {
return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdateWithHistoryRewrite(theResource, theRequest, theTransactionDetails), onRollback);
} else {
return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
}
}
private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
@ -1845,6 +1849,59 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return outcome;
}
/**
* Method for updating the historical version of the resource when a history version id is included in the request.
*
* @param theResource to be saved
* @param theRequest details of the request
* @param theTransactionDetails details of the transaction
* @return the outcome of the operation
*/
private DaoMethodOutcome doUpdateWithHistoryRewrite(T theResource, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
StopWatch w = new StopWatch();
// No need for indexing as this will update a non-current version of the resource which will not be searchable
preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, false);
BaseHasResource entity = null;
BaseHasResource currentEntity = null;
IIdType resourceId;
resourceId = theResource.getIdElement();
assert resourceId != null;
assert resourceId.hasIdPart();
try {
currentEntity = readEntityLatestVersion(resourceId.toVersionless(), theRequest, theTransactionDetails);
if (!resourceId.hasVersionIdPart()) {
throw new InvalidRequestException(Msg.code(2093) + "Invalid resource ID, ID must contain a history version");
}
entity = readEntity(resourceId, theRequest);
validateResourceType(entity);
} catch (ResourceNotFoundException e) {
throw new ResourceNotFoundException(Msg.code(2087) + "Resource not found [" + resourceId + "] - Doesn't exist");
}
if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
throw new UnprocessableEntityException(Msg.code(2088) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]");
}
assert resourceId.hasVersionIdPart();
boolean wasDeleted = entity.getDeleted() != null;
entity.setDeleted(null);
boolean isUpdatingCurrent = resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) == currentEntity.getVersion();
IBasePersistedResource savedEntity = updateHistoryEntity(theRequest, theResource, currentEntity, entity, resourceId, theTransactionDetails, isUpdatingCurrent);
DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, theResource).setCreated(wasDeleted);
String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
ourLog.debug(msg);
return outcome;
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequest) {

View File

@ -260,6 +260,11 @@ public class JpaConstants {
public static final String HEADER_UPSERT_EXISTENCE_CHECK = "X-Upsert-Extistence-Check";
public static final String HEADER_UPSERT_EXISTENCE_CHECK_DISABLED = "disabled";
/**
* Parameters for the rewrite history operation
*/
public static final String HEADER_REWRITE_HISTORY = "X-Rewrite-History";
/**
* Non-instantiable
*/

View File

@ -0,0 +1,248 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.Date;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
public class FhirResourceDaoR4HistoryRewriteTest extends BaseJpaR4Test {
private static final String TEST_SYSTEM_NAME = "testHistoryRewrite";
private static final String TEST_FAMILY_NAME = "Johnson";
private static final String TEST_GIVEN_NAME = "Dan";
private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4Test.class);
@BeforeEach
public void setUp() {
myDaoConfig.setUpdateWithHistoryRewriteEnabled(true);
}
@AfterEach
public void tearDown() {
myDaoConfig.setUpdateWithHistoryRewriteEnabled(false);
when(mySrd.getHeader(eq(JpaConstants.HEADER_REWRITE_HISTORY))).thenReturn("");
}
@Test
public void testHistoryRewriteNonCurrentVersion() {
String systemNameModified = "testHistoryRewriteDiff";
String testFamilyNameModified = "Jackson";
// setup
IIdType id = createPatientWithHistory();
// execute updates
when(mySrd.getHeader(eq(JpaConstants.HEADER_REWRITE_HISTORY))).thenReturn("true");
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(testFamilyNameModified);
p.setId("Patient/" + id.getIdPart() + "/_history/2");
Patient history2 = myPatientDao.read(id.withVersion("2"));
String versionBeforeUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
ourLog.debug("Patient history 2: {}", history2);
myPatientDao.update(p, mySrd);
String versionAfterUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
assertEquals(versionBeforeUpdate, versionAfterUpdate);
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(systemNameModified);
p.setId("Patient/" + id.getIdPart() + "/_history/1");
Patient history1 = myPatientDao.read(id.withVersion("1"));
ourLog.debug("Patient history 1: {}", history1);
versionBeforeUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
myPatientDao.update(p, mySrd);
versionAfterUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
assertEquals(versionBeforeUpdate, versionAfterUpdate);
Patient h2 = myPatientDao.read(id.withVersion("2"), mySrd);
assertEquals(testFamilyNameModified, h2.getName().get(0).getFamily());
assertThat(h2.getIdElement().toString(), endsWith("/_history/2"));
assertTrue(Math.abs(h2.getMeta().getLastUpdated().getTime() - new Date().getTime()) < 1000L);
Patient h1 = myPatientDao.read(id.withVersion("1"), mySrd);
assertEquals(systemNameModified, h1.getIdentifier().get(0).getValue());
assertThat(h1.getIdElement().toString(), endsWith("/_history/1"));
assertTrue(Math.abs(h1.getMeta().getLastUpdated().getTime() - new Date().getTime()) < 1000L);
}
@Test
public void testHistoryRewriteCurrentVersion() {
String testFamilyNameModified = "Jackson";
String testGivenNameModified = "Randy";
// setup
IIdType id = createPatientWithHistory();
int resourceVersionsSizeInit = myResourceHistoryTableDao.findAll().size();
// execute update
when(mySrd.getHeader(eq(JpaConstants.HEADER_REWRITE_HISTORY))).thenReturn("true");
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(testFamilyNameModified).setGiven(List.of(new StringType(testGivenNameModified)));
p.setId("Patient/" + id.getIdPart() + "/_history/3");
String versionBeforeUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
myPatientDao.update(p, mySrd);
String versionAfterUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
assertEquals(versionBeforeUpdate, versionAfterUpdate);
int resourceVersionsSizeAfterUpdate = myResourceHistoryTableDao.findAll().size();
Patient lPatient = myPatientDao.read(id.toVersionless(), mySrd);
assertEquals(testFamilyNameModified, lPatient.getName().get(0).getFamily());
assertEquals(testGivenNameModified, lPatient.getName().get(0).getGiven().get(0).getValue());
assertEquals(resourceVersionsSizeInit, resourceVersionsSizeAfterUpdate);
assertThat(lPatient.getIdElement().toString(), endsWith("/_history/3"));
assertTrue(Math.abs(lPatient.getMeta().getLastUpdated().getTime() - new Date().getTime()) < 1000L);
}
@Test
public void testHistoryRewriteNoCustomHeader() {
String testFamilyNameModified = "Jackson";
// setup
IIdType id = createPatientWithHistory();
// execute update
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(testFamilyNameModified);
p.setId("Patient/" + id.getIdPart() + "/_history/2");
try {
myPatientDao.update(p, mySrd);
fail();
} catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), containsString("but this is not the current version"));
}
}
@Test
public void testHistoryRewriteNonExistingId() {
String testFamilyNameModified = "Jackson";
// setup
IIdType id = createPatientWithHistory();
// execute update
when(mySrd.getHeader(eq(JpaConstants.HEADER_REWRITE_HISTORY))).thenReturn("true");
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(testFamilyNameModified);
p.setId("Patient/WrongId");
try {
myPatientDao.update(p, mySrd);
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), containsString("Doesn't exist"));
}
}
@Test
public void testHistoryRewriteNonExistingVersion() {
String testFamilyNameModified = "Jackson";
// setup
IIdType id = createPatientWithHistory();
// execute update
when(mySrd.getHeader(eq(JpaConstants.HEADER_REWRITE_HISTORY))).thenReturn("true");
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(testFamilyNameModified);
p.setId("Patient/" + id.getIdPart() + "/_history/4");
try {
myPatientDao.update(p, mySrd);
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), containsString("Doesn't exist"));
}
}
@Test
public void testHistoryRewriteNoHistoryVersion() {
String testFamilyNameModified = "Jackson";
// setup
IIdType id = createPatientWithHistory();
// execute update
when(mySrd.getHeader(eq(JpaConstants.HEADER_REWRITE_HISTORY))).thenReturn("true");
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(testFamilyNameModified);
p.setId("Patient/" + id.getIdPart());
try {
myPatientDao.update(p, mySrd);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Invalid resource ID, ID must contain a history version"));
}
}
@Nonnull
private IIdType createPatientWithHistory() {
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ourLog.info("Created patient, got it: {}", id);
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(TEST_FAMILY_NAME);
p.setId("Patient/" + id.getIdPart());
String versionBeforeUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
myPatientDao.update(p, mySrd);
String versionAfterUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
assertNotEquals(versionBeforeUpdate, versionAfterUpdate);
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(TEST_FAMILY_NAME).setGiven(List.of(new StringType(TEST_GIVEN_NAME)));
p.setId("Patient/" + id.getIdPart());
versionBeforeUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
myPatientDao.update(p, mySrd);
versionAfterUpdate = myPatientDao.read(id.toUnqualifiedVersionless()).getIdElement().getVersionIdPart();
assertNotEquals(versionBeforeUpdate, versionAfterUpdate);
p = myPatientDao.read(id.toVersionless(), mySrd);
assertEquals(TEST_FAMILY_NAME, p.getName().get(0).getFamily());
assertThat(p.getIdElement().toString(), endsWith("/_history/3"));
return id;
}
}

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.test.config.TestR4Config;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.parser.IParser;
@ -145,6 +146,7 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nonnull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@ -224,6 +226,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
myClient.unregisterInterceptor(myCapturingInterceptor);
myDaoConfig.setUpdateWithHistoryRewriteEnabled(false);
}
@BeforeEach
@ -6690,6 +6693,134 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
}
@Test
public void testUpdateHistoryRewriteWithIdNoHistoryVersion() {
myDaoConfig.setUpdateWithHistoryRewriteEnabled(true);
String testFamilyNameModified = "Jackson";
// setup
IIdType id = createNewPatientWithHistory();
// execute updates
Patient p = new Patient();
p.setActive(true);
p.addName().setFamily(testFamilyNameModified);
try {
myClient.update().resource(p).historyRewrite().withId(id).execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("ID must contain a history version"));
}
try {
myClient.update().resource(p).historyRewrite().withId("1234").execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("ID must contain a history version"));
}
p.setId(id);
try {
myClient.update().resource(p).historyRewrite().execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("ID must contain a history version"));
}
}
@Test
public void testUpdateHistoryRewriteWithIdNull() {
myDaoConfig.setUpdateWithHistoryRewriteEnabled(true);
String testFamilyNameModified = "Jackson";
// setup
createNewPatientWithHistory();
// execute updates
Patient p = new Patient();
p.setActive(true);
p.addName().setFamily(testFamilyNameModified);
try {
myClient.update().resource(p).historyRewrite().withId((IIdType) null).execute();
fail();
} catch (NullPointerException e) {
assertThat(e.getMessage(), containsString("can not be null"));
}
try {
myClient.update().resource(p).historyRewrite().withId((String) null).execute();
fail();
} catch (NullPointerException e) {
assertThat(e.getMessage(), containsString("can not be null"));
}
try {
myClient.update().resource(p).historyRewrite().execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("No ID supplied for resource to update"));
}
}
@Test
public void testUpdateHistoryRewriteWithIdNoIdPart() {
myDaoConfig.setUpdateWithHistoryRewriteEnabled(true);
String testFamilyNameModified = "Jackson";
// setup
createNewPatientWithHistory();
// execute updates
Patient p = new Patient();
p.setActive(true);
p.addName().setFamily(testFamilyNameModified);
IIdType noIdPartId = new IdDt();
try {
myClient.update().resource(p).historyRewrite().withId(noIdPartId).execute();
fail();
} catch (NullPointerException e) {
assertThat(e.getMessage(), containsString("must not be blank and must contain an ID"));
}
try {
myClient.update().resource(p).historyRewrite().withId("").execute();
fail();
} catch (NullPointerException e) {
assertThat(e.getMessage(), containsString("must not be blank and must contain an ID"));
}
p.setId(noIdPartId);
try {
myClient.update().resource(p).historyRewrite().execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("No ID supplied for resource to update"));
}
}
@Nonnull
private IIdType createNewPatientWithHistory() {
String TEST_SYSTEM_NAME = "testHistoryRewrite";
String TEST_FAMILY_NAME = "Johnson";
Patient p = new Patient();
p.setActive(true);
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
IIdType id = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
ourLog.info("Created patient, got it: {}", id);
p = new Patient();
p.setActive(true);
p.addIdentifier().setSystem("urn:system").setValue(TEST_SYSTEM_NAME);
p.addName().setFamily(TEST_FAMILY_NAME);
p.setId("Patient/" + id.getIdPart());
myClient.update().resource(p).execute();
return id;
}
private String toStr(Date theDate) {
return new InstantDt(theDate).getValueAsString();
}

View File

@ -127,4 +127,11 @@ public interface IAuthRuleBuilderRule {
* @since 5.5.0
*/
IAuthRuleBuilderRuleBulkExport bulkExport();
/**
* This rule specifically allows a user to perform a FHIR update on the historical version of a resource
*
* @since 6.1.0
*/
IAuthRuleBuilderUpdateHistoryRewrite updateHistoryRewrite();
}

View File

@ -0,0 +1,26 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IAuthRuleBuilderUpdateHistoryRewrite {
IAuthRuleFinished allRequests();
}

View File

@ -318,6 +318,11 @@ public class RuleBuilder implements IAuthRuleBuilder {
return new RuleBuilderBulkExport();
}
@Override
public IAuthRuleBuilderUpdateHistoryRewrite updateHistoryRewrite() {
return new UpdateHistoryRewriteBuilder();
}
private class RuleBuilderRuleConditional implements IAuthRuleBuilderRuleConditional {
private AppliesTypeEnum myAppliesTo;
@ -751,6 +756,22 @@ public class RuleBuilder implements IAuthRuleBuilder {
}
}
private class UpdateHistoryRewriteBuilder implements IAuthRuleBuilderUpdateHistoryRewrite {
UpdateHistoryRewriteBuilder() {
super();
}
@Override
public IAuthRuleFinished allRequests() {
BaseRule rule = new RuleImplUpdateHistoryRewrite(myRuleName)
.setAllRequests(true)
.setMode(myRuleMode);
myRules.add(rule);
return new RuleBuilderFinished(rule);
}
}
private class RuleBuilderGraphQL implements IAuthRuleBuilderGraphQL {
@Override
public IAuthRuleFinished any() {

View File

@ -1,10 +1,10 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
@ -175,6 +175,9 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
if (theInputResource == null && theInputResourceId == null) {
return null;
}
if (theRequestDetails.getId() != null && theRequestDetails.getId().hasVersionIdPart() && theOperation == RestOperationTypeEnum.UPDATE) {
return null;
}
switch (theOperation) {
case CREATE:
case UPDATE:

View File

@ -0,0 +1,55 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Set;
public class RuleImplUpdateHistoryRewrite extends BaseRule {
private boolean myAllRequests;
RuleImplUpdateHistoryRewrite(String theRuleName) {
super(theRuleName);
}
@Override
public AuthorizationInterceptor.Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags, Pointcut thePointcut) {
if (myAllRequests) {
if (theRequestDetails.getId() != null && theRequestDetails.getId().hasVersionIdPart() && theOperation == RestOperationTypeEnum.UPDATE) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
}
}
return null;
}
RuleImplUpdateHistoryRewrite setAllRequests(boolean theAllRequests) {
myAllRequests = theAllRequests;
return this;
}
}

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.maintenance;
/*-
* #%L
* HAPI FHIR JPA Server - Batch2 Task Processor
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.batch2.model.StatusEnum;
import ca.uhn.fhir.batch2.model.WorkChunk;

View File

@ -1,12 +1,32 @@
package ca.uhn.fhir.batch2.maintenance;
import ca.uhn.fhir.batch2.progress.JobInstanceProgressCalculator;
/*-
* #%L
* HAPI FHIR JPA Server - Batch2 Task Processor
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.batch2.api.IJobPersistence;
import ca.uhn.fhir.batch2.channel.BatchJobSender;
import ca.uhn.fhir.batch2.model.JobInstance;
import ca.uhn.fhir.batch2.model.JobWorkCursor;
import ca.uhn.fhir.batch2.model.JobWorkNotification;
import ca.uhn.fhir.batch2.model.StatusEnum;
import ca.uhn.fhir.batch2.progress.JobInstanceProgressCalculator;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.progress;
/*-
* #%L
* HAPI FHIR JPA Server - Batch2 Task Processor
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.batch2.api.IJobCompletionHandler;
import ca.uhn.fhir.batch2.api.JobCompletionDetails;
import ca.uhn.fhir.batch2.model.JobDefinition;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.progress;
/*-
* #%L
* HAPI FHIR JPA Server - Batch2 Task Processor
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.batch2.api.IJobPersistence;
import ca.uhn.fhir.batch2.maintenance.JobChunkProgressAccumulator;
import ca.uhn.fhir.batch2.maintenance.JobMaintenanceServiceImpl;

View File

@ -314,6 +314,11 @@ public class DaoConfig {
*/
private int myBulkExportFileRetentionPeriodHours = 2;
/**
* Since 6.1.0
*/
private boolean myUpdateWithHistoryRewriteEnabled = false;
/**
* Constructor
*/
@ -2873,7 +2878,7 @@ public class DaoConfig {
*/
public int getBulkExportFileRetentionPeriodHours() {
return myBulkExportFileRetentionPeriodHours;
}
}
/**
* This setting controls how long Bulk Export collection entities will be retained after job start.
@ -2881,11 +2886,32 @@ public class DaoConfig {
*
* @since 6.0.0
*/
public void setBulkExportFileRetentionPeriodHours(int theBulkExportFileRetentionPeriodHours) {
myBulkExportFileRetentionPeriodHours = theBulkExportFileRetentionPeriodHours;
}
public void setBulkExportFileRetentionPeriodHours(int theBulkExportFileRetentionPeriodHours) {
myBulkExportFileRetentionPeriodHours = theBulkExportFileRetentionPeriodHours;
}
public enum StoreMetaSourceInformationEnum {
/**
* This setting indicates whether updating the history of a resource is allowed.
* Default is false.
*
* @since 6.1.0
*/
public boolean isUpdateWithHistoryRewriteEnabled() {
return myUpdateWithHistoryRewriteEnabled;
}
/**
* This setting indicates whether updating the history of a resource is allowed.
* Default is false.
*
* @since 6.1.0
*/
public void setUpdateWithHistoryRewriteEnabled(boolean theUpdateWithHistoryRewriteEnabled) {
myUpdateWithHistoryRewriteEnabled = theUpdateWithHistoryRewriteEnabled;
}
public enum StoreMetaSourceInformationEnum {
NONE(false, false),
SOURCE_URI(true, false),
REQUEST_ID(false, true),