mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-02-08 14:05:02 +00:00
merge master into branch
This commit is contained in:
commit
3d3bab5102
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: change
|
||||||
|
jira: SMILE-2927
|
||||||
|
title: "During transactions, any resources that were PUT or POSTed with a conditional URL now receive extra validation. There is now a final
|
||||||
|
storage step which ensures that the stored resource actually matches the conditional URL."
|
@ -52,13 +52,11 @@ public class LazyDaoMethodOutcome extends DaoMethodOutcome {
|
|||||||
|
|
||||||
private void tryToRunSupplier() {
|
private void tryToRunSupplier() {
|
||||||
if (myEntitySupplier != null) {
|
if (myEntitySupplier != null) {
|
||||||
|
|
||||||
EntityAndResource entityAndResource = myEntitySupplier.get();
|
EntityAndResource entityAndResource = myEntitySupplier.get();
|
||||||
setEntity(entityAndResource.getEntity());
|
setEntity(entityAndResource.getEntity());
|
||||||
setResource(entityAndResource.getResource());
|
setResource(entityAndResource.getResource());
|
||||||
setId(entityAndResource.getResource().getIdElement());
|
setId(entityAndResource.getResource().getIdElement());
|
||||||
myEntitySupplierUseCallback.run();
|
myEntitySupplierUseCallback.run();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
|||||||
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
|
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
|
||||||
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
|
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
|
||||||
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
|
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
|
||||||
|
import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome;
|
||||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||||
import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
|
import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
|
||||||
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
|
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
|
||||||
@ -44,7 +45,9 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
|||||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||||
import ca.uhn.fhir.model.api.IResource;
|
import ca.uhn.fhir.model.api.IResource;
|
||||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||||
import ca.uhn.fhir.parser.DataFormatException;
|
import ca.uhn.fhir.parser.DataFormatException;
|
||||||
@ -64,6 +67,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
|||||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
|
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
|
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
|
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
|
||||||
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
||||||
@ -80,6 +84,7 @@ import ca.uhn.fhir.util.UrlUtil;
|
|||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.hl7.fhir.dstu3.model.Bundle;
|
import org.hl7.fhir.dstu3.model.Bundle;
|
||||||
import org.hl7.fhir.exceptions.FHIRException;
|
import org.hl7.fhir.exceptions.FHIRException;
|
||||||
@ -156,6 +161,8 @@ public abstract class BaseTransactionProcessor {
|
|||||||
private ModelConfig myModelConfig;
|
private ModelConfig myModelConfig;
|
||||||
@Autowired
|
@Autowired
|
||||||
private InMemoryResourceMatcher myInMemoryResourceMatcher;
|
private InMemoryResourceMatcher myInMemoryResourceMatcher;
|
||||||
|
@Autowired
|
||||||
|
private SearchParamMatcher mySearchParamMatcher;
|
||||||
|
|
||||||
private TaskExecutor myExecutor ;
|
private TaskExecutor myExecutor ;
|
||||||
|
|
||||||
@ -274,7 +281,9 @@ public abstract class BaseTransactionProcessor {
|
|||||||
idSubstitutions.put(id, newId);
|
idSubstitutions.put(id, newId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
idToPersistedOutcome.put(newId, outcome);
|
|
||||||
|
populateIdToPersistedOutcomeMap(idToPersistedOutcome, newId, outcome);
|
||||||
|
|
||||||
if (outcome.getCreated()) {
|
if (outcome.getCreated()) {
|
||||||
myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED));
|
myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED));
|
||||||
} else {
|
} else {
|
||||||
@ -298,6 +307,21 @@ public abstract class BaseTransactionProcessor {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Method which populates entry in idToPersistedOutcome.
|
||||||
|
* Will store whatever outcome is sent, unless the key already exists, then we only replace an instance if we find that the instance
|
||||||
|
* we are replacing with is non-lazy. This allows us to evaluate later more easily, as we _know_ we need access to these.
|
||||||
|
*/
|
||||||
|
private void populateIdToPersistedOutcomeMap(Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType newId, DaoMethodOutcome outcome) {
|
||||||
|
//Prefer real method outcomes over lazy ones.
|
||||||
|
if (idToPersistedOutcome.containsKey(newId)) {
|
||||||
|
if (!(outcome instanceof LazyDaoMethodOutcome)) {
|
||||||
|
idToPersistedOutcome.put(newId, outcome);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idToPersistedOutcome.put(newId, outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Date getLastModified(IBaseResource theRes) {
|
private Date getLastModified(IBaseResource theRes) {
|
||||||
return theRes.getMeta().getLastUpdated();
|
return theRes.getMeta().getLastUpdated();
|
||||||
}
|
}
|
||||||
@ -481,7 +505,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
entries.sort(new TransactionSorter(placeholderIds));
|
entries.sort(new TransactionSorter(placeholderIds));
|
||||||
|
|
||||||
// perform all writes
|
// perform all writes
|
||||||
doTransactionWriteOperations(theRequestDetails, theActionName,
|
prepareThenExecuteTransactionWriteOperations(theRequestDetails, theActionName,
|
||||||
transactionDetails, transactionStopWatch,
|
transactionDetails, transactionStopWatch,
|
||||||
response, originalRequestOrder, entries);
|
response, originalRequestOrder, entries);
|
||||||
|
|
||||||
@ -584,38 +608,15 @@ public abstract class BaseTransactionProcessor {
|
|||||||
* heavy load with lots of concurrent transactions using all available
|
* heavy load with lots of concurrent transactions using all available
|
||||||
* database connections.
|
* database connections.
|
||||||
*/
|
*/
|
||||||
private void doTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName,
|
private void prepareThenExecuteTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName,
|
||||||
TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch,
|
TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch,
|
||||||
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
||||||
List<IBase> theEntries) {
|
List<IBase> theEntries) {
|
||||||
|
|
||||||
TransactionWriteOperationsDetails writeOperationsDetails = null;
|
TransactionWriteOperationsDetails writeOperationsDetails = null;
|
||||||
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, myInterceptorBroadcaster, theRequestDetails) ||
|
if (haveWriteOperationsHooks(theRequestDetails)) {
|
||||||
CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, myInterceptorBroadcaster, theRequestDetails)) {
|
writeOperationsDetails = buildWriteOperationsDetails(theEntries);
|
||||||
|
callWriteOperationsHook(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, theRequestDetails, theTransactionDetails, writeOperationsDetails);
|
||||||
List<String> updateRequestUrls = new ArrayList<>();
|
|
||||||
List<String> conditionalCreateRequestUrls = new ArrayList<>();
|
|
||||||
for (IBase nextEntry : theEntries) {
|
|
||||||
String method = myVersionAdapter.getEntryRequestVerb(myContext, nextEntry);
|
|
||||||
if ("PUT".equals(method)) {
|
|
||||||
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextEntry);
|
|
||||||
if (isNotBlank(requestUrl)) {
|
|
||||||
updateRequestUrls.add(requestUrl);
|
|
||||||
}
|
|
||||||
} else if ("POST".equals(method)) {
|
|
||||||
String requestUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextEntry);
|
|
||||||
if (isNotBlank(requestUrl) && requestUrl.contains("?")) {
|
|
||||||
conditionalCreateRequestUrls.add(requestUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeOperationsDetails = new TransactionWriteOperationsDetails();
|
|
||||||
writeOperationsDetails.setUpdateRequestUrls(updateRequestUrls);
|
|
||||||
writeOperationsDetails.setConditionalCreateRequestUrls(conditionalCreateRequestUrls);
|
|
||||||
HookParams params = new HookParams()
|
|
||||||
.add(TransactionDetails.class, theTransactionDetails)
|
|
||||||
.add(TransactionWriteOperationsDetails.class, writeOperationsDetails);
|
|
||||||
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TransactionCallback<Map<IBase, IIdType>> txCallback = status -> {
|
TransactionCallback<Map<IBase, IIdType>> txCallback = status -> {
|
||||||
@ -636,13 +637,9 @@ public abstract class BaseTransactionProcessor {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
entriesToProcess = myHapiTransactionService.execute(theRequestDetails, theTransactionDetails, txCallback);
|
entriesToProcess = myHapiTransactionService.execute(theRequestDetails, theTransactionDetails, txCallback);
|
||||||
}
|
} finally {
|
||||||
finally {
|
if (haveWriteOperationsHooks(theRequestDetails)) {
|
||||||
if (writeOperationsDetails != null) {
|
callWriteOperationsHook(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, theRequestDetails, theTransactionDetails, writeOperationsDetails);
|
||||||
HookParams params = new HookParams()
|
|
||||||
.add(TransactionDetails.class, theTransactionDetails)
|
|
||||||
.add(TransactionWriteOperationsDetails.class, writeOperationsDetails);
|
|
||||||
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, params);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,6 +653,45 @@ public abstract class BaseTransactionProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean haveWriteOperationsHooks(RequestDetails theRequestDetails) {
|
||||||
|
return CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, myInterceptorBroadcaster, theRequestDetails) ||
|
||||||
|
CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, myInterceptorBroadcaster, theRequestDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void callWriteOperationsHook(Pointcut thePointcut, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, TransactionWriteOperationsDetails theWriteOperationsDetails) {
|
||||||
|
HookParams params = new HookParams()
|
||||||
|
.add(TransactionDetails.class, theTransactionDetails)
|
||||||
|
.add(TransactionWriteOperationsDetails.class, theWriteOperationsDetails);
|
||||||
|
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, thePointcut, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private TransactionWriteOperationsDetails buildWriteOperationsDetails(List<IBase> theEntries) {
|
||||||
|
TransactionWriteOperationsDetails writeOperationsDetails;
|
||||||
|
List<String> updateRequestUrls = new ArrayList<>();
|
||||||
|
List<String> conditionalCreateRequestUrls = new ArrayList<>();
|
||||||
|
//Extract
|
||||||
|
for (IBase nextEntry : theEntries) {
|
||||||
|
String method = myVersionAdapter.getEntryRequestVerb(myContext, nextEntry);
|
||||||
|
if ("PUT".equals(method)) {
|
||||||
|
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextEntry);
|
||||||
|
if (isNotBlank(requestUrl)) {
|
||||||
|
updateRequestUrls.add(requestUrl);
|
||||||
|
}
|
||||||
|
} else if ("POST".equals(method)) {
|
||||||
|
String requestUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextEntry);
|
||||||
|
if (isNotBlank(requestUrl) && requestUrl.contains("?")) {
|
||||||
|
conditionalCreateRequestUrls.add(requestUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOperationsDetails = new TransactionWriteOperationsDetails();
|
||||||
|
writeOperationsDetails.setUpdateRequestUrls(updateRequestUrls);
|
||||||
|
writeOperationsDetails.setConditionalCreateRequestUrls(conditionalCreateRequestUrls);
|
||||||
|
return writeOperationsDetails;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isValidVerb(String theVerb) {
|
private boolean isValidVerb(String theVerb) {
|
||||||
try {
|
try {
|
||||||
return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null;
|
return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null;
|
||||||
@ -704,7 +740,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
|
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
||||||
String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
|
String entryFullUrl = myVersionAdapter.getFullUrl(nextReqEntry);
|
||||||
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
|
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
|
||||||
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
|
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
|
||||||
String key = verb + "|" + requestUrl + "|" + ifNoneExist;
|
String key = verb + "|" + requestUrl + "|" + ifNoneExist;
|
||||||
@ -712,7 +748,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
// Conditional UPDATE
|
// Conditional UPDATE
|
||||||
boolean consolidateEntry = false;
|
boolean consolidateEntry = false;
|
||||||
if ("PUT".equals(verb)) {
|
if ("PUT".equals(verb)) {
|
||||||
if (isNotBlank(entryUrl) && isNotBlank(requestUrl)) {
|
if (isNotBlank(entryFullUrl) && isNotBlank(requestUrl)) {
|
||||||
int questionMarkIndex = requestUrl.indexOf('?');
|
int questionMarkIndex = requestUrl.indexOf('?');
|
||||||
if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
|
if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
|
||||||
consolidateEntry = true;
|
consolidateEntry = true;
|
||||||
@ -722,8 +758,8 @@ public abstract class BaseTransactionProcessor {
|
|||||||
|
|
||||||
// Conditional CREATE
|
// Conditional CREATE
|
||||||
if ("POST".equals(verb)) {
|
if ("POST".equals(verb)) {
|
||||||
if (isNotBlank(entryUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
|
if (isNotBlank(entryFullUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
|
||||||
if (!entryUrl.equals(requestUrl)) {
|
if (!entryFullUrl.equals(requestUrl)) {
|
||||||
consolidateEntry = true;
|
consolidateEntry = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -731,12 +767,24 @@ public abstract class BaseTransactionProcessor {
|
|||||||
|
|
||||||
if (consolidateEntry) {
|
if (consolidateEntry) {
|
||||||
if (!keyToUuid.containsKey(key)) {
|
if (!keyToUuid.containsKey(key)) {
|
||||||
keyToUuid.put(key, entryUrl);
|
keyToUuid.put(key, entryFullUrl);
|
||||||
} else {
|
} else {
|
||||||
ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
|
ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
|
||||||
theEntries.remove(index);
|
theEntries.remove(index);
|
||||||
index--;
|
index--;
|
||||||
String existingUuid = keyToUuid.get(key);
|
String existingUuid = keyToUuid.get(key);
|
||||||
|
replaceReferencesInEntriesWithConsolidatedUUID(theEntries, entryFullUrl, existingUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over all entries, and if it finds any which have references which match the fullUrl of the entry that was consolidated out
|
||||||
|
* replace them with our new consolidated UUID
|
||||||
|
*/
|
||||||
|
private void replaceReferencesInEntriesWithConsolidatedUUID(List<IBase> theEntries, String theEntryFullUrl, String existingUuid) {
|
||||||
for (IBase nextEntry : theEntries) {
|
for (IBase nextEntry : theEntries) {
|
||||||
IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
|
IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
|
||||||
for (IBaseReference nextReference : myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) {
|
for (IBaseReference nextReference : myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) {
|
||||||
@ -746,17 +794,13 @@ public abstract class BaseTransactionProcessor {
|
|||||||
if (isBlank(nextReferenceId) && nextReference.getResource() != null) {
|
if (isBlank(nextReferenceId) && nextReference.getResource() != null) {
|
||||||
nextReferenceId = nextReference.getResource().getIdElement().getValue();
|
nextReferenceId = nextReference.getResource().getIdElement().getValue();
|
||||||
}
|
}
|
||||||
if (entryUrl.equals(nextReferenceId)) {
|
if (theEntryFullUrl.equals(nextReferenceId)) {
|
||||||
nextReference.setReference(existingUuid);
|
nextReference.setReference(existingUuid);
|
||||||
nextReference.setResource(null);
|
nextReference.setResource(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the next resource id (IIdType) from the base resource and next request entry.
|
* Retrieves the next resource id (IIdType) from the base resource and next request entry.
|
||||||
@ -810,12 +854,16 @@ public abstract class BaseTransactionProcessor {
|
|||||||
return nextResourceId;
|
return nextResourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** After pre-hooks have been called
|
||||||
|
*
|
||||||
|
*/
|
||||||
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
|
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
|
||||||
TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
||||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||||
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
||||||
List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
||||||
|
|
||||||
|
// During a transaction, we don't execute hooks, instead, we execute them all post-transaction.
|
||||||
theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts(
|
theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts(
|
||||||
Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED,
|
Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED,
|
||||||
Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED,
|
Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED,
|
||||||
@ -827,6 +875,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
Map<IBase, IIdType> entriesToProcess = new IdentityHashMap<>();
|
Map<IBase, IIdType> entriesToProcess = new IdentityHashMap<>();
|
||||||
Set<IIdType> nonUpdatedEntities = new HashSet<>();
|
Set<IIdType> nonUpdatedEntities = new HashSet<>();
|
||||||
Set<IBasePersistedResource> updatedEntities = new HashSet<>();
|
Set<IBasePersistedResource> updatedEntities = new HashSet<>();
|
||||||
|
Map<String, IIdType> conditionalUrlToIdMap = new HashMap<>();
|
||||||
List<IBaseResource> updatedResources = new ArrayList<>();
|
List<IBaseResource> updatedResources = new ArrayList<>();
|
||||||
Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>();
|
Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>();
|
||||||
|
|
||||||
@ -866,6 +915,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
|
String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
|
||||||
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
|
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
|
||||||
outcome = resourceDao.create(res, matchUrl, false, theTransactionDetails, theRequest);
|
outcome = resourceDao.create(res, matchUrl, false, theTransactionDetails, theRequest);
|
||||||
|
setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId());
|
||||||
res.setId(outcome.getId());
|
res.setId(outcome.getId());
|
||||||
if (nextResourceId != null) {
|
if (nextResourceId != null) {
|
||||||
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
|
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
|
||||||
@ -900,6 +950,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
String matchUrl = parts.getResourceType() + '?' + parts.getParams();
|
String matchUrl = parts.getResourceType() + '?' + parts.getParams();
|
||||||
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
|
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
|
||||||
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequest);
|
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequest);
|
||||||
|
setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, deleteOutcome.getId());
|
||||||
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
|
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
|
||||||
for (ResourceTable deleted : allDeleted) {
|
for (ResourceTable deleted : allDeleted) {
|
||||||
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
|
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
|
||||||
@ -942,6 +993,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
}
|
}
|
||||||
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
|
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
|
||||||
outcome = resourceDao.update(res, matchUrl, false, false, theRequest, theTransactionDetails);
|
outcome = resourceDao.update(res, matchUrl, false, false, theRequest, theTransactionDetails);
|
||||||
|
setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId());
|
||||||
if (Boolean.TRUE.equals(outcome.getCreated())) {
|
if (Boolean.TRUE.equals(outcome.getCreated())) {
|
||||||
conditionalRequestUrls.put(matchUrl, res.getClass());
|
conditionalRequestUrls.put(matchUrl, res.getClass());
|
||||||
}
|
}
|
||||||
@ -1000,6 +1052,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
|
IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
|
||||||
IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId());
|
IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId());
|
||||||
DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, patchBodyParameters, theRequest);
|
DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, patchBodyParameters, theRequest);
|
||||||
|
setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId());
|
||||||
updatedEntities.add(outcome.getEntity());
|
updatedEntities.add(outcome.getEntity());
|
||||||
if (outcome.getResource() != null) {
|
if (outcome.getResource() != null) {
|
||||||
updatedResources.add(outcome.getResource());
|
updatedResources.add(outcome.getResource());
|
||||||
@ -1056,6 +1109,15 @@ public abstract class BaseTransactionProcessor {
|
|||||||
if (!myDaoConfig.isMassIngestionMode()) {
|
if (!myDaoConfig.isMassIngestionMode()) {
|
||||||
validateNoDuplicates(theRequest, theActionName, conditionalRequestUrls, theIdToPersistedOutcome.values());
|
validateNoDuplicates(theRequest, theActionName, conditionalRequestUrls, theIdToPersistedOutcome.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
theTransactionStopWatch.endCurrentTask();
|
||||||
|
if (conditionalUrlToIdMap.size() > 0) {
|
||||||
|
theTransactionStopWatch.startTask("Check that all conditionally created/updated entities actually match their conditionals.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!myDaoConfig.isMassIngestionMode()) {
|
||||||
|
validateAllInsertsMatchTheirConditionalUrls(theIdToPersistedOutcome, conditionalUrlToIdMap, theRequest);
|
||||||
|
}
|
||||||
theTransactionStopWatch.endCurrentTask();
|
theTransactionStopWatch.endCurrentTask();
|
||||||
|
|
||||||
for (IIdType next : theAllIds) {
|
for (IIdType next : theAllIds) {
|
||||||
@ -1094,6 +1156,39 @@ public abstract class BaseTransactionProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setConditionalUrlToBeValidatedLater(Map<String, IIdType> theConditionalUrlToIdMap, String theMatchUrl, IIdType theId) {
|
||||||
|
if (!StringUtils.isBlank(theMatchUrl)) {
|
||||||
|
theConditionalUrlToIdMap.put(theMatchUrl, theId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After transaction processing and resolution of indexes and references, we want to validate that the resources that were stored _actually_
|
||||||
|
* match the conditional URLs that they were brought in on.
|
||||||
|
* @param theIdToPersistedOutcome
|
||||||
|
* @param conditionalUrlToIdMap
|
||||||
|
*/
|
||||||
|
private void validateAllInsertsMatchTheirConditionalUrls(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<String, IIdType> conditionalUrlToIdMap, RequestDetails theRequest) {
|
||||||
|
conditionalUrlToIdMap.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getKey() != null)
|
||||||
|
.forEach(entry -> {
|
||||||
|
String matchUrl = entry.getKey();
|
||||||
|
IIdType value = entry.getValue();
|
||||||
|
DaoMethodOutcome daoMethodOutcome = theIdToPersistedOutcome.get(value);
|
||||||
|
if (daoMethodOutcome != null && !daoMethodOutcome.isNop() && daoMethodOutcome.getResource() != null) {
|
||||||
|
InMemoryMatchResult match = mySearchParamMatcher.match(matchUrl, daoMethodOutcome.getResource(), theRequest);
|
||||||
|
if (ourLog.isDebugEnabled()) {
|
||||||
|
ourLog.debug("Checking conditional URL [{}] against resource with ID [{}]: Supported?:[{}], Matched?:[{}]", matchUrl, value, match.supported(), match.matched());
|
||||||
|
}
|
||||||
|
if (match.supported()) {
|
||||||
|
if (!match.matched()) {
|
||||||
|
throw new PreconditionFailedException("Invalid conditional URL \"" + matchUrl + "\". The given resource is not matched by this URL.");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for any delete conflicts.
|
* Checks for any delete conflicts.
|
||||||
* @param theDeleteConflicts - set of delete conflicts
|
* @param theDeleteConflicts - set of delete conflicts
|
||||||
@ -1384,7 +1479,7 @@ public abstract class BaseTransactionProcessor {
|
|||||||
thePersistedOutcomes
|
thePersistedOutcomes
|
||||||
.stream()
|
.stream()
|
||||||
.filter(t -> !t.isNop())
|
.filter(t -> !t.isNop())
|
||||||
.filter(t -> t.getEntity() instanceof ResourceTable)
|
.filter(t -> t.getEntity() instanceof ResourceTable)//N.B. GGG: This validation never occurs for mongo, as nothing is a ResourceTable.
|
||||||
.filter(t -> t.getEntity().getDeleted() == null)
|
.filter(t -> t.getEntity().getDeleted() == null)
|
||||||
.filter(t -> t.getResource() != null)
|
.filter(t -> t.getResource() != null)
|
||||||
.forEach(t -> resourceToIndexedParams.put(t.getResource(), new ResourceIndexedSearchParams((ResourceTable) t.getEntity())));
|
.forEach(t -> resourceToIndexedParams.put(t.getResource(), new ResourceIndexedSearchParams((ResourceTable) t.getEntity())));
|
||||||
|
@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
|||||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.hibernate.internal.SessionImpl;
|
import org.hibernate.internal.SessionImpl;
|
||||||
@ -72,6 +73,8 @@ public class TransactionProcessorTest {
|
|||||||
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
||||||
@MockBean
|
@MockBean
|
||||||
private IResourceVersionSvc myResourceVersionSvc;
|
private IResourceVersionSvc myResourceVersionSvc;
|
||||||
|
@MockBean
|
||||||
|
private SearchParamMatcher mySearchParamMatcher;
|
||||||
|
|
||||||
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
|
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private SessionImpl mySession;
|
private SessionImpl mySession;
|
||||||
|
@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.param.TokenParam;
|
|||||||
import ca.uhn.fhir.util.BundleBuilder;
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r4.model.BooleanType;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.Condition;
|
import org.hl7.fhir.r4.model.Condition;
|
||||||
import org.hl7.fhir.r4.model.Encounter;
|
import org.hl7.fhir.r4.model.Encounter;
|
||||||
@ -467,7 +468,8 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||||||
|
|
||||||
Patient patient = new Patient();
|
Patient patient = new Patient();
|
||||||
patient.setId(IdType.newRandomUuid());
|
patient.setId(IdType.newRandomUuid());
|
||||||
patient.setActive(true);
|
patient.setDeceased(new BooleanType(true));
|
||||||
|
patient.setActive(false);
|
||||||
builder
|
builder
|
||||||
.addTransactionUpdateEntry(patient)
|
.addTransactionUpdateEntry(patient)
|
||||||
.conditional("Patient?active=false");
|
.conditional("Patient?active=false");
|
||||||
|
@ -51,7 +51,9 @@ import org.hl7.fhir.r4.model.DateTimeType;
|
|||||||
import org.hl7.fhir.r4.model.DiagnosticReport;
|
import org.hl7.fhir.r4.model.DiagnosticReport;
|
||||||
import org.hl7.fhir.r4.model.Encounter;
|
import org.hl7.fhir.r4.model.Encounter;
|
||||||
import org.hl7.fhir.r4.model.EpisodeOfCare;
|
import org.hl7.fhir.r4.model.EpisodeOfCare;
|
||||||
|
import org.hl7.fhir.r4.model.HumanName;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
import org.hl7.fhir.r4.model.Medication;
|
import org.hl7.fhir.r4.model.Medication;
|
||||||
import org.hl7.fhir.r4.model.MedicationRequest;
|
import org.hl7.fhir.r4.model.MedicationRequest;
|
||||||
import org.hl7.fhir.r4.model.Meta;
|
import org.hl7.fhir.r4.model.Meta;
|
||||||
@ -83,6 +85,7 @@ import java.io.InputStream;
|
|||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -96,7 +99,9 @@ import static org.hamcrest.Matchers.containsString;
|
|||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
import static org.hamcrest.Matchers.emptyString;
|
import static org.hamcrest.Matchers.emptyString;
|
||||||
import static org.hamcrest.Matchers.endsWith;
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.greaterThan;
|
import static org.hamcrest.Matchers.greaterThan;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.matchesPattern;
|
import static org.hamcrest.Matchers.matchesPattern;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.hamcrest.Matchers.startsWith;
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
@ -1161,6 +1166,78 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
|||||||
validate(outcome);
|
validate(outcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConditionalUpdate_forObservationWithNonExistentPatientSubject_shouldCreateLinkedResources() {
|
||||||
|
Bundle transactionBundle = new Bundle().setType(BundleType.TRANSACTION);
|
||||||
|
|
||||||
|
// Patient
|
||||||
|
HumanName patientName = new HumanName().setFamily("TEST_LAST_NAME").addGiven("TEST_FIRST_NAME");
|
||||||
|
Identifier patientIdentifier = new Identifier().setSystem("http://example.com/mrns").setValue("U1234567890");
|
||||||
|
Patient patient = new Patient()
|
||||||
|
.setName(Arrays.asList(patientName))
|
||||||
|
.setIdentifier(Arrays.asList(patientIdentifier));
|
||||||
|
patient.setId(IdType.newRandomUuid());
|
||||||
|
|
||||||
|
transactionBundle
|
||||||
|
.addEntry()
|
||||||
|
.setFullUrl(patient.getId())
|
||||||
|
.setResource(patient)
|
||||||
|
.getRequest()
|
||||||
|
.setMethod(Bundle.HTTPVerb.PUT)
|
||||||
|
.setUrl("/Patient?identifier=" + patientIdentifier.getSystem() + "|" + patientIdentifier.getValue());
|
||||||
|
|
||||||
|
// Observation
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.setId(IdType.newRandomUuid());
|
||||||
|
observation.getSubject().setReference(patient.getIdElement().toUnqualifiedVersionless().toString());
|
||||||
|
|
||||||
|
transactionBundle
|
||||||
|
.addEntry()
|
||||||
|
.setFullUrl(observation.getId())
|
||||||
|
.setResource(observation)
|
||||||
|
.getRequest()
|
||||||
|
.setMethod(Bundle.HTTPVerb.PUT)
|
||||||
|
.setUrl("/Observation?subject=" + patient.getIdElement().toUnqualifiedVersionless().toString());
|
||||||
|
|
||||||
|
ourLog.info("Patient TEMP UUID: {}", patient.getId());
|
||||||
|
String s = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(transactionBundle);
|
||||||
|
System.out.println(s);
|
||||||
|
Bundle outcome= mySystemDao.transaction(null, transactionBundle);
|
||||||
|
String patientLocation = outcome.getEntry().get(0).getResponse().getLocation();
|
||||||
|
assertThat(patientLocation, matchesPattern("Patient/[a-z0-9-]+/_history/1"));
|
||||||
|
String observationLocation = outcome.getEntry().get(1).getResponse().getLocation();
|
||||||
|
assertThat(observationLocation, matchesPattern("Observation/[a-z0-9-]+/_history/1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConditionalUrlWhichDoesNotMatchResource() {
|
||||||
|
Bundle transactionBundle = new Bundle().setType(BundleType.TRANSACTION);
|
||||||
|
|
||||||
|
String storedIdentifierValue = "woop";
|
||||||
|
String conditionalUrlIdentifierValue = "zoop";
|
||||||
|
// Patient
|
||||||
|
HumanName patientName = new HumanName().setFamily("TEST_LAST_NAME").addGiven("TEST_FIRST_NAME");
|
||||||
|
Identifier patientIdentifier = new Identifier().setSystem("http://example.com/mrns").setValue(storedIdentifierValue);
|
||||||
|
Patient patient = new Patient()
|
||||||
|
.setName(Arrays.asList(patientName))
|
||||||
|
.setIdentifier(Arrays.asList(patientIdentifier));
|
||||||
|
patient.setId(IdType.newRandomUuid());
|
||||||
|
|
||||||
|
transactionBundle
|
||||||
|
.addEntry()
|
||||||
|
.setFullUrl(patient.getId())
|
||||||
|
.setResource(patient)
|
||||||
|
.getRequest()
|
||||||
|
.setMethod(Bundle.HTTPVerb.PUT)
|
||||||
|
.setUrl("/Patient?identifier=" + patientIdentifier.getSystem() + "|" + conditionalUrlIdentifierValue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mySystemDao.transaction(null, transactionBundle);
|
||||||
|
fail();
|
||||||
|
} catch (PreconditionFailedException e) {
|
||||||
|
assertThat(e.getMessage(), is(equalTo("Invalid conditional URL \"Patient?identifier=http://example.com/mrns|" + conditionalUrlIdentifierValue +"\". The given resource is not matched by this URL.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransactionCreateInlineMatchUrlWithOneMatch() {
|
public void testTransactionCreateInlineMatchUrlWithOneMatch() {
|
||||||
@ -1195,6 +1272,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransactionUpdateTwoResourcesWithSameId() {
|
public void testTransactionUpdateTwoResourcesWithSameId() {
|
||||||
Bundle request = new Bundle();
|
Bundle request = new Bundle();
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"url": "/Practitioner?identifier=http%253A%252F%252Facme.org%252Fclinicians%257C777"
|
"url": "/Practitioner?identifier=http%3A%2F%2Facme.org%2Fclinicians%7C777"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -125,7 +125,7 @@
|
|||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"url": "/Patient?identifier=http%253A%252F%252Facme.org%252Fmrns%257C7000135"
|
"url": "/Patient?identifier=http%3A%2F%2Facme.org%2Fmrns%7C7000135"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -203,7 +203,7 @@
|
|||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"url": "/Encounter?identifier=http%253A%252F%252Facme.org%252FvisitNumbers%257C4736455"
|
"url": "/Encounter?identifier=http%3A%2F%2Facme.org%2FvisitNumbers%7C4736455"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -267,7 +267,7 @@
|
|||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"url": "/Practitioner?identifier=http%253A%252F%252Facme.org%252Fclinicians%257C3622"
|
"url": "/Practitioner?identifier=http%3A%2F%2Facme.org%2Fclinicians%7C3622"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -312,7 +312,7 @@
|
|||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"url": "/Practitioner?identifier=http%253A%252F%252Facme.org%252Fclinicians%257C7452"
|
"url": "/Practitioner?identifier=http%3A%2F%2Facme.org%2Fclinicians%7C7452"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -61,7 +61,6 @@ import org.hl7.fhir.r4.model.Organization;
|
|||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.hl7.fhir.r4.model.Practitioner;
|
import org.hl7.fhir.r4.model.Practitioner;
|
||||||
import org.hl7.fhir.r4.model.Reference;
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
@ -156,6 +156,7 @@ public class InMemoryResourceMatcher {
|
|||||||
String resourceName = theResourceDefinition.getName();
|
String resourceName = theResourceDefinition.getName();
|
||||||
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
|
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
|
||||||
InMemoryMatchResult checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, paramDef, theAndOrParams);
|
InMemoryMatchResult checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, paramDef, theAndOrParams);
|
||||||
|
|
||||||
if (!checkUnsupportedResult.supported()) {
|
if (!checkUnsupportedResult.supported()) {
|
||||||
return checkUnsupportedResult;
|
return checkUnsupportedResult;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthR
|
|||||||
* <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li>
|
* <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li>
|
||||||
* <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li>
|
* <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
>*
|
||||||
* @param theId The ID of the resource to apply (e.g. <code>Patient/123</code>)
|
* @param theId The ID of the resource to apply (e.g. <code>Patient/123</code>)
|
||||||
* @throws IllegalArgumentException If theId does not contain an ID with at least an ID part
|
* @throws IllegalArgumentException If theId does not contain an ID with at least an ID part
|
||||||
* @throws NullPointerException If theId is null
|
* @throws NullPointerException If theId is null
|
||||||
|
Loading…
x
Reference in New Issue
Block a user