Avoid SQL based transaction dupe check (#2688)

* Avoid SQL based transaction dupe check

* Add changelog

* Test fixes

* Test fixes
This commit is contained in:
James Agnew 2021-05-30 20:26:36 -04:00 committed by GitHub
parent 786112284b
commit a2950324ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 245 additions and 38 deletions

View File

@ -250,8 +250,16 @@ public class UrlUtil {
}
public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException {
int paramIndex = theUrl.indexOf('?');
String resourceName = theUrl.substring(0, paramIndex);
String url = theUrl;
int paramIndex = url.indexOf('?');
// Change pattern of "Observation/?param=foo" into "Observation?param=foo"
if (paramIndex > 0 && url.charAt(paramIndex - 1) == '/') {
url = url.substring(0, paramIndex - 1) + url.substring(paramIndex);
paramIndex--;
}
String resourceName = url.substring(0, paramIndex);
if (resourceName.contains("/")) {
resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
}

View File

@ -31,7 +31,7 @@
title: "JSON parser no longer allows the resource ID to be specified in an element called \"_id\" (the correct one is \"id\"). Previously _id was allowed because some early FHIR examples used that form, but this was never actually valid so it is now being removed."
- item:
type: "add"
title: "JPA server now allows \"forced IDs\" (ids containing non-numeric, client assigned IDs) to use the same logical ID part on different resource types. E.g. A server may now have both Patient/foo and Obervation/foo on the same server. <br/><br/> Note that existing databases will need to modify index \"IDX_FORCEDID\" as it is no longer unique, and perform a reindexing pass."
title: "JPA server now allows \"forced IDs\" (ids containing non-numeric, client assigned IDs) to use the same logical ID part on different resource types. E.g. A server may now have both Patient/foo and Observation/foo on the same server. <br/><br/> Note that existing databases will need to modify index \"IDX_FORCEDID\" as it is no longer unique, and perform a reindexing pass."
- item:
issue: "350"
type: "fix"

View File

@ -0,0 +1,5 @@
---
type: perf
issue: 2688
title: "FHIR Transaction duplicate record checks are now performed without any database interactions or SQL statements,
reducing the processing load associated with FHIR transactions by at least a small amount."

View File

@ -0,0 +1,5 @@
---
type: perf
issue: 2688
title: "Conditional URL lookups in the JPA server will now explicitly specify a maximum fetch size of 2, avoiding
fetching more data that won't be used inadvertently in some situations."

View File

@ -1447,7 +1447,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public Set<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest) {
return myTransactionService.execute(theRequest, tx -> {
theParams.setLoadSynchronousUpTo(getConfig().getInternalSynchronousSearchSize());
if (theParams.getLoadSynchronousUpTo() != null) {
theParams.setLoadSynchronousUpTo(Math.min(getConfig().getInternalSynchronousSearchSize(), theParams.getLoadSynchronousUpTo()));
} else {
theParams.setLoadSynchronousUpTo(getConfig().getInternalSynchronousSearchSize());
}
ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType());
@ -1573,7 +1578,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity = myEntityManager.find(ResourceTable.class, pid.getId());
resourceId = entity.getIdDt();
} else {
return create(resource, null, thePerformIndexing, theTransactionDetails, theRequest);
DaoMethodOutcome outcome = create(resource, null, thePerformIndexing, theTransactionDetails, theRequest);
// Pre-cache the match URL
if (outcome.getPersistentId() != null) {
myMatchResourceUrlService.matchUrlResolved(theMatchUrl, outcome.getPersistentId());
}
return outcome;
}
} else {
/*

View File

@ -39,7 +39,8 @@ import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException;
@ -50,7 +51,6 @@ import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
@ -65,6 +65,7 @@ import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ServletRequestUtil;
import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.FhirTerser;
@ -97,6 +98,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
@ -109,6 +111,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import static ca.uhn.fhir.util.StringUtil.toUtf8String;
import static org.apache.commons.lang3.StringUtils.defaultString;
@ -118,6 +121,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseTransactionProcessor {
public static final String URN_PREFIX = "urn:";
public static final Pattern UNQUALIFIED_MATCH_URL_START = Pattern.compile("^[a-zA-Z0-9_]+=");
private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class);
private BaseHapiFhirDao myDao;
@Autowired
@ -138,6 +142,8 @@ public abstract class BaseTransactionProcessor {
private DaoConfig myDaoConfig;
@Autowired
private ModelConfig myModelConfig;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
@PostConstruct
public void start() {
@ -937,7 +943,9 @@ public abstract class BaseTransactionProcessor {
if (conditionalRequestUrls.size() > 0) {
theTransactionStopWatch.startTask("Check for conflicts in conditional resources");
}
validateNoDuplicates(theRequest, theActionName, conditionalRequestUrls);
if (!myDaoConfig.isMassIngestionMode()) {
validateNoDuplicates(theRequest, theActionName, conditionalRequestUrls, theIdToPersistedOutcome.values());
}
theTransactionStopWatch.endCurrentTask();
for (IIdType next : theAllIds) {
@ -986,15 +994,15 @@ public abstract class BaseTransactionProcessor {
* in the database. This is trickier than you'd think because of a couple of possibilities during the
* save:
* * There may be resources that have not changed (e.g. an update/PUT with a resource body identical
* to what is already in the database)
* to what is already in the database)
* * There may be resources with auto-versioned references, meaning we're replacing certain references
* in the resource with a versioned references, referencing the current version at the time of the
* transaction processing
* in the resource with a versioned references, referencing the current version at the time of the
* transaction processing
* * There may by auto-versioned references pointing to these unchanged targets
*
* <p>
* If we're not doing any auto-versioned references, we'll just iterate through all resources in the
* transaction and save them one at a time.
*
* <p>
* However, if we have any auto-versioned references we do this in 2 passes: First the resources from the
* transaction that don't have any auto-versioned references are stored. We do them first since there's
* a chance they may be a NOP and we'll need to account for their version number not actually changing.
@ -1162,15 +1170,51 @@ public abstract class BaseTransactionProcessor {
}
}
private void validateNoDuplicates(RequestDetails theRequest, String theActionName, Map<String, Class<? extends IBaseResource>> conditionalRequestUrls) {
private void validateNoDuplicates(RequestDetails theRequest, String theActionName, Map<String, Class<? extends IBaseResource>> conditionalRequestUrls, Collection<DaoMethodOutcome> thePersistedOutcomes) {
IdentityHashMap<IBaseResource, ResourceIndexedSearchParams> resourceToIndexedParams = new IdentityHashMap<>(thePersistedOutcomes.size());
thePersistedOutcomes
.stream()
.filter(t -> !t.isNop())
.filter(t -> t.getEntity() instanceof ResourceTable)
.filter(t -> t.getEntity().getDeleted() == null)
.filter(t -> t.getResource() != null)
.forEach(t -> resourceToIndexedParams.put(t.getResource(), new ResourceIndexedSearchParams((ResourceTable) t.getEntity())));
for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
String matchUrl = nextEntry.getKey();
Class<? extends IBaseResource> resType = nextEntry.getValue();
if (isNotBlank(matchUrl)) {
Set<ResourcePersistentId> val = myMatchResourceUrlService.processMatchUrl(matchUrl, resType, theRequest);
if (val.size() > 1) {
throw new InvalidRequestException(
"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
if (matchUrl.startsWith("?") || (!matchUrl.contains("?") && UNQUALIFIED_MATCH_URL_START.matcher(matchUrl).find())) {
StringBuilder b = new StringBuilder();
b.append(myContext.getResourceType(nextEntry.getValue()));
if (!matchUrl.startsWith("?")) {
b.append("?");
}
b.append(matchUrl);
matchUrl = b.toString();
}
if (!myInMemoryResourceMatcher.canBeEvaluatedInMemory(matchUrl).supported()) {
continue;
}
int counter = 0;
for (Map.Entry<IBaseResource, ResourceIndexedSearchParams> entries : resourceToIndexedParams.entrySet()) {
ResourceIndexedSearchParams indexedParams = entries.getValue();
IBaseResource resource = entries.getKey();
String resourceType = myContext.getResourceType(resource);
if (!matchUrl.startsWith(resourceType + "?")) {
continue;
}
if (myInMemoryResourceMatcher.match(matchUrl, resource, indexedParams).matched()) {
counter++;
if (counter > 1) {
throw new InvalidRequestException(
"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
}
}
}
}
}

View File

@ -62,6 +62,9 @@ public class MatchResourceUrlService {
@Autowired
private MemoryCacheService myMemoryCacheService;
/**
* Note that this will only return a maximum of 2 results!!
*/
public <R extends IBaseResource> Set<ResourcePersistentId> processMatchUrl(String theMatchUrl, Class<R> theResourceType, RequestDetails theRequest) {
if (myDaoConfig.getMatchUrlCache()) {
ResourcePersistentId existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl);
@ -75,7 +78,7 @@ public class MatchResourceUrlService {
if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
}
paramMap.setLoadSynchronous(true);
paramMap.setLoadSynchronousUpTo(2);
Set<ResourcePersistentId> retVal = search(paramMap, theResourceType, theRequest);

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
@ -53,6 +54,8 @@ public class TransactionProcessorTest {
private HapiTransactionService myHapiTransactionService;
@MockBean
private ModelConfig myModelConfig;
@MockBean
private InMemoryResourceMatcher myInMemoryResourceMatcher;
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
private SessionImpl mySession;

View File

@ -45,6 +45,7 @@ import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
@ -714,7 +715,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
myCaptureQueriesListener.clear();
mySystemDao.transaction(mySrd, bundleCreator.get());
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
@ -775,7 +776,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
myCaptureQueriesListener.clear();
mySystemDao.transaction(mySrd, bundleCreator.get());
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
myCaptureQueriesListener.logSelectQueries();
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
@ -793,6 +795,11 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
assertEquals(3, myCaptureQueriesListener.countInsertQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
// Make sure the match URL query uses a small limit
String matchUrlQuery = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, true);
assertThat(matchUrlQuery, containsString("t0.HASH_SYS_AND_VALUE = '-4132452001562191669'"));
assertThat(matchUrlQuery, containsString("limit '2'"));
runInTransaction(()->{
List<String> types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList());
assertThat(types, containsInAnyOrder("Patient", "Observation", "Observation"));
@ -1550,7 +1557,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
myCaptureQueriesListener.clear();
mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
// myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(5, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(13, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
@ -10,7 +9,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
@ -49,7 +47,6 @@ import org.hl7.fhir.r4.model.Communication;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.EpisodeOfCare;
import org.hl7.fhir.r4.model.IdType;
@ -88,12 +85,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyString;
@ -104,7 +99,6 @@ import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -833,7 +827,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
request.addEntry().getRequest().setMethod(HTTPVerb.GET).setUrl("Patient?identifier=foo");
try {
runInTransaction(()->{
runInTransaction(() -> {
mySystemDao.transactionNested(mySrd, request);
});
fail();
@ -886,6 +880,128 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testTransactionWithConditionalCreates_IdenticalMatchUrlsDifferentTypes_Unqualified() {
BundleBuilder bb = new BundleBuilder(myFhirCtx);
Patient pt = new Patient();
pt.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(pt).conditional("identifier=foo|bar");
Observation obs = new Observation();
obs.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(obs).conditional("identifier=foo|bar");
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("201 Created", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+/_history/1"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), matchesPattern(".*Observation/[0-9]+/_history/1"));
// Take 2
bb = new BundleBuilder(myFhirCtx);
pt = new Patient();
pt.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(pt).conditional("identifier=foo|bar");
obs = new Observation();
obs.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(obs).conditional("identifier=foo|bar");
outcome = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+/_history/1"));
assertEquals("200 OK", outcome.getEntry().get(1).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), matchesPattern(".*Observation/[0-9]+/_history/1"));
}
@Test
public void testTransactionWithConditionalCreates_IdenticalMatchUrlsDifferentTypes_Qualified() {
BundleBuilder bb = new BundleBuilder(myFhirCtx);
Patient pt = new Patient();
pt.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(pt).conditional("Patient?identifier=foo|bar");
Observation obs = new Observation();
obs.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(obs).conditional("Observation?identifier=foo|bar");
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("201 Created", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+/_history/1"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), matchesPattern(".*Observation/[0-9]+/_history/1"));
// Take 2
bb = new BundleBuilder(myFhirCtx);
pt = new Patient();
pt.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(pt).conditional("Patient?identifier=foo|bar");
obs = new Observation();
obs.addIdentifier().setSystem("foo").setValue("bar");
bb.addTransactionCreateEntry(obs).conditional("Observation?identifier=foo|bar");
outcome = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+/_history/1"));
assertEquals("200 OK", outcome.getEntry().get(1).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), matchesPattern(".*Observation/[0-9]+/_history/1"));
}
@Test
public void testTransactionWithConditionalCreate_NoResourceTypeInUrl() {
BundleBuilder bb = new BundleBuilder(myFhirCtx);
Patient pt = new Patient();
pt.setActive(true);
bb.addTransactionCreateEntry(pt).conditional("active=true");
pt = new Patient();
pt.setActive(false);
bb.addTransactionCreateEntry(pt).conditional("active=false");
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("201 Created", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
// Take 2
bb = new BundleBuilder(myFhirCtx);
pt = new Patient();
pt.setActive(true);
bb.addTransactionCreateEntry(pt).conditional("active=true");
pt = new Patient();
pt.setActive(false);
bb.addTransactionCreateEntry(pt).conditional("active=false");
Bundle outcome2 = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("200 OK", outcome2.getEntry().get(0).getResponse().getStatus());
assertEquals("200 OK", outcome2.getEntry().get(1).getResponse().getStatus());
assertThat(outcome.getEntry().get(0).getResponse().getLocation(), endsWith("/_history/1"));
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("/_history/1"));
assertEquals(outcome.getEntry().get(0).getResponse().getLocation(), outcome2.getEntry().get(0).getResponse().getLocation());
assertEquals(outcome.getEntry().get(1).getResponse().getLocation(), outcome2.getEntry().get(1).getResponse().getLocation());
// Take 3
bb = new BundleBuilder(myFhirCtx);
pt = new Patient();
pt.setActive(true);
bb.addTransactionCreateEntry(pt).conditional("?active=true");
pt = new Patient();
pt.setActive(false);
bb.addTransactionCreateEntry(pt).conditional("?active=false");
Bundle outcome3 = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle());
assertEquals("200 OK", outcome3.getEntry().get(0).getResponse().getStatus());
assertEquals("200 OK", outcome3.getEntry().get(1).getResponse().getStatus());
assertEquals(outcome.getEntry().get(0).getResponse().getLocation(), outcome3.getEntry().get(0).getResponse().getLocation());
assertEquals(outcome.getEntry().get(1).getResponse().getLocation(), outcome3.getEntry().get(1).getResponse().getLocation());
}
@Test
public void testTransactionNoContained() throws IOException {

View File

@ -726,7 +726,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
Subscription subscription2 = createSubscription(criteria2, payload);
waitForActivatedSubscriptionCount(2);
ourLog.info("** About to send obervation");
ourLog.info("** About to send observation");
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
@ -798,7 +798,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
createSubscription(criteria1, payload);
waitForActivatedSubscriptionCount(1);
ourLog.info("** About to send obervation");
ourLog.info("** About to send observation");
Observation observation = new Observation();
observation.addIdentifier().setSystem("foo").setValue("bar1");
@ -820,7 +820,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
.setResource(observation)
.setFullUrl(observation.getId())
.getRequest()
.setUrl("Obervation?identifier=foo|bar1")
.setUrl("Observation?identifier=foo|bar1")
.setMethod(Bundle.HTTPVerb.PUT);
requestBundle.addEntry()
.setResource(patient)

View File

@ -593,7 +593,7 @@ public class RestHookTestR5Test extends BaseSubscriptionsR5Test {
Subscription subscription2 = createSubscription(criteria2, payload);
waitForActivatedSubscriptionCount(2);
ourLog.info("** About to send obervation");
ourLog.info("** About to send observation");
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
@ -668,7 +668,7 @@ public class RestHookTestR5Test extends BaseSubscriptionsR5Test {
createSubscription(criteria1, payload);
waitForActivatedSubscriptionCount(1);
ourLog.info("** About to send obervation");
ourLog.info("** About to send observation");
Observation observation = new Observation();
observation.addIdentifier().setSystem("foo").setValue("bar1");
@ -690,7 +690,7 @@ public class RestHookTestR5Test extends BaseSubscriptionsR5Test {
.setResource(observation)
.setFullUrl(observation.getId())
.getRequest()
.setUrl("Obervation?identifier=foo|bar1")
.setUrl("Observation?identifier=foo|bar1")
.setMethod(Bundle.HTTPVerb.PUT);
requestBundle.addEntry()
.setResource(patient)

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.MetaUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.model.Location;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -49,6 +50,7 @@ import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
public class InMemoryResourceMatcher {
@ -73,6 +75,8 @@ public class InMemoryResourceMatcher {
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
RuntimeResourceDefinition resourceDefinition;
if (theResource == null) {
Validate.isTrue(!theCriteria.startsWith("?"), "Invalid match URL format (must match \"[resourceType]?[params]\")");
Validate.isTrue(theCriteria.contains("?"), "Invalid match URL format (must match \"[resourceType]?[params]\")");
resourceDefinition = UrlUtil.parseUrlResourceType(myFhirContext, theCriteria);
} else {
resourceDefinition = myFhirContext.getResourceDefinition(theResource);

View File

@ -9,7 +9,7 @@
"resourceType": "DiagnosticReport",
"text": {
"status": "generated",
"div": "\u003cdiv xmlns\u003d\"http://www.w3.org/1999/xhtml\"\u003e\n\t\t\t\t\t\t\u003ch3\u003eHLA-A,-B,-C genotyping report for Mary Chalmers (MRN:12345)\u003c/h3\u003e\n\t\t\t\t\t\t\u003cpre\u003e\n LOCUS ALLELE 1 ALLELE 2\n HLA-A HLA-A:01:01G HLA-A*01:02\n HLA-B HLA-B*15:01:01G HLA-B*57:01:01G\n HLA-C HLA-C*01:02:01G HLA-C*03:04:01G\n \u003c/pre\u003e\n\t\t\t\t\t\t\u003cp\u003eAllele assignments based on IMGT/HLA 3.23\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eEffective date: 2015-12-15\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eMethod: Sequencing of exons 2 and 3 of HLA Class I genes\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eLab: aTypingLab Inc\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eNote: Please refer the \u003ca href\u003d\"genomics.html#hla\"\u003eimplementation guide \u003c/a\u003e for more explanation on this\n carefully organized bundle example.\u003c/p\u003e\n\t\t\t\t\t\t\u003cpre\u003e\n Bob Milius - NMDP - 2016-12-01\n\n Transaction bundle that creates and links:\n + DiagnosticReport summarizing genotyping for HLA-A,-B,-C typing of patient(donor)\n + Obervations for each genotype\n + Observations for each allele\n + Sequences for exons 2 and 3 for HLA-A,-B, -C\n\n The endpoints of the following resources are hardcoded into this transaction bundle\n because these are presumed to already exist when developing the DiagnosticRequest\n which was to generate this report bundle:\n\n Patient/119 (potential donor)\n Specimen/120 (buccal swab)\n Organization/68 (typing lab)\n ServiceRequest/123 (report is based on this request)\n \u003c/pre\u003e\n\t\t\t\t\t\u003c/div\u003e"
"div": "\u003cdiv xmlns\u003d\"http://www.w3.org/1999/xhtml\"\u003e\n\t\t\t\t\t\t\u003ch3\u003eHLA-A,-B,-C genotyping report for Mary Chalmers (MRN:12345)\u003c/h3\u003e\n\t\t\t\t\t\t\u003cpre\u003e\n LOCUS ALLELE 1 ALLELE 2\n HLA-A HLA-A:01:01G HLA-A*01:02\n HLA-B HLA-B*15:01:01G HLA-B*57:01:01G\n HLA-C HLA-C*01:02:01G HLA-C*03:04:01G\n \u003c/pre\u003e\n\t\t\t\t\t\t\u003cp\u003eAllele assignments based on IMGT/HLA 3.23\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eEffective date: 2015-12-15\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eMethod: Sequencing of exons 2 and 3 of HLA Class I genes\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eLab: aTypingLab Inc\u003c/p\u003e\n\t\t\t\t\t\t\u003cp\u003eNote: Please refer the \u003ca href\u003d\"genomics.html#hla\"\u003eimplementation guide \u003c/a\u003e for more explanation on this\n carefully organized bundle example.\u003c/p\u003e\n\t\t\t\t\t\t\u003cpre\u003e\n Bob Milius - NMDP - 2016-12-01\n\n Transaction bundle that creates and links:\n + DiagnosticReport summarizing genotyping for HLA-A,-B,-C typing of patient(donor)\n + Observations for each genotype\n + Observations for each allele\n + Sequences for exons 2 and 3 for HLA-A,-B, -C\n\n The endpoints of the following resources are hardcoded into this transaction bundle\n because these are presumed to already exist when developing the DiagnosticRequest\n which was to generate this report bundle:\n\n Patient/119 (potential donor)\n Specimen/120 (buccal swab)\n Organization/68 (typing lab)\n ServiceRequest/123 (report is based on this request)\n \u003c/pre\u003e\n\t\t\t\t\t\u003c/div\u003e"
},
"extension": [
{
@ -1213,4 +1213,4 @@
}
]
}
}
}