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:
parent
bf2f126c91
commit
3da39aeda3
|
@ -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() {}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
@ -150,6 +149,10 @@ public enum RestOperationTypeEnum {
|
|||
*/
|
||||
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>();
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -43,4 +43,5 @@ public interface IUpdateTyped extends IUpdateExecutable {
|
|||
*/
|
||||
IUpdateWithQuery conditional();
|
||||
|
||||
IUpdateTyped historyRewrite();
|
||||
}
|
||||
|
|
|
@ -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,8 +2289,17 @@ 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");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -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}}
|
||||
|
|
|
@ -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,17 +1708,28 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
|||
|
||||
// Notify interceptors
|
||||
if (!savedEntity.isUnchangedInCurrentOperation()) {
|
||||
hookParams = new 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)
|
||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
|
||||
if (!isUnchanged) {
|
||||
hookParams.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED;
|
||||
}
|
||||
|
||||
return savedEntity;
|
||||
doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams);
|
||||
}
|
||||
|
||||
protected void addPidToResource(IBasePersistedResource theEntity, IBaseResource theResource) {
|
||||
|
|
|
@ -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,8 +1712,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
Runnable onRollback = () -> theResource.getIdElement().setValue(id);
|
||||
|
||||
// Execute the update in a retryable transaction
|
||||
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) {
|
||||
StopWatch w = new StopWatch();
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -314,6 +314,11 @@ public class DaoConfig {
|
|||
*/
|
||||
private int myBulkExportFileRetentionPeriodHours = 2;
|
||||
|
||||
/**
|
||||
* Since 6.1.0
|
||||
*/
|
||||
private boolean myUpdateWithHistoryRewriteEnabled = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
|
@ -2885,6 +2890,27 @@ public class DaoConfig {
|
|||
myBulkExportFileRetentionPeriodHours = theBulkExportFileRetentionPeriodHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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),
|
||||
|
|
Loading…
Reference in New Issue