Merge remote-tracking branch 'origin/master' into 2325-channel-import-mimetype

This commit is contained in:
Tadgh 2021-01-29 11:02:54 -05:00
commit 3cc9d08ebc
128 changed files with 2113 additions and 1052 deletions

View File

@ -8,6 +8,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import org.hl7.fhir.dstu3.model.*;
import org.junit.jupiter.api.*; import static org.hamcrest.MatcherAssert.assertThat;
@ -99,6 +100,7 @@ public class GenericClientDstu3IT {
.build();
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
client.setEncoding(EncodingEnum.JSON);
Patient pt = new Patient();
pt.getText().setDivAsString("A PATIENT");
@ -131,6 +133,8 @@ public class GenericClientDstu3IT {
ArgumentCaptor<Request> capt = prepareClientForSearchResponse();
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
client.setEncoding(EncodingEnum.JSON);
int idx = 0;
client
@ -143,7 +147,7 @@ public class GenericClientDstu3IT {
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(idx).url().toString());
assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(idx).url().toString());
idx++;
}
@ -170,6 +174,7 @@ public class GenericClientDstu3IT {
.build();
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
client.setEncoding(EncodingEnum.JSON);
Binary bin = new Binary();
bin.setContent(new byte[] { 0, 1, 2, 3, 4 });
@ -178,7 +183,7 @@ public class GenericClientDstu3IT {
Request request = capt.getAllValues().get(0);
ourLog.info(request.headers().toString());
assertEquals("http://example.com/fhir/Binary", request.url().toString());
assertEquals("http://example.com/fhir/Binary?_format=json", request.url().toString());
validateUserAgent(capt);
assertEquals(Constants.CT_FHIR_JSON_NEW + ";charset=utf-8", request.body().contentType().toString().toLowerCase().replace(" ", ""));
@ -242,6 +247,7 @@ public class GenericClientDstu3IT {
.build();
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
client.setEncoding(EncodingEnum.JSON);
Patient pt = new Patient();
pt.getText().setDivAsString("A PATIENT");
@ -251,8 +257,9 @@ public class GenericClientDstu3IT {
assertNull(outcome.getOperationOutcome());
assertNotNull(outcome.getResource());
assertEquals(1, capt.getAllValues().size());
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">FINAL VALUE</div>", ((Patient) outcome.getResource()).getText().getDivAsString());
assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).url().toString());
assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(0).url().toString());
}
@ -271,673 +278,6 @@ public class GenericClientDstu3IT {
}
/*
@Test
public void testForceConformance() throws Exception {
final IParser p = ourCtx.newJsonParser();
final Conformance conf = new Conformance();
conf.setCopyright("COPY");
final Patient patient = new Patient();
patient.addName().addFamily("FAM");
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8"));
when(myHttpResponse.execute().body()).thenReturn(ResponseBody.create(MediaType.parse(Constants.CT_FHIR_XML + "; charset=UTF-8"), respString));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
private int myCount = 0;
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
final String respString;
if (myCount == 1 || myCount == 2) {
ourLog.info("Encoding patient");
respString = p.encodeResourceToString(patient);
} else {
ourLog.info("Encoding conformance");
respString = p.encodeResourceToString(conf);
}
myCount++;
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE);
IGenericClient client = ourCtx.newRestfulGenericClient("http://testForceConformance.com/fhir");
client.read().resource("Patient").withId("1").execute();
assertEquals(2, capt.getAllValues().size());
assertEquals("http://testForceConformance.com/fhir/metadata?_format=json", capt.getAllValues().get(0).getURI().toASCIIString());
assertEquals("http://testForceConformance.com/fhir/Patient/1?_format=json", capt.getAllValues().get(1).getURI().toASCIIString());
client.read().resource("Patient").withId("1").execute();
assertEquals(3, capt.getAllValues().size());
assertEquals("http://testForceConformance.com/fhir/Patient/1?_format=json", capt.getAllValues().get(2).getURI().toASCIIString());
client.forceConformanceCheck();
assertEquals(4, capt.getAllValues().size());
assertEquals("http://testForceConformance.com/fhir/metadata?_format=json", capt.getAllValues().get(3).getURI().toASCIIString());
}
@Test
public void testHttp499() throws Exception {
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 499, "Wacky Message"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader("HELLO"), StandardCharsets.UTF_8);
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.read().resource(Patient.class).withId("1").execute();
fail();
} catch (UnclassifiedServerFailureException e) {
assertEquals("ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException: HTTP 499 Wacky Message", e.toString());
assertEquals("HELLO", e.getResponseBody());
}
}
@Test
public void testHttp501() throws Exception {
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 501, "Not Implemented"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader("not implemented"), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.read().resource(Patient.class).withId("1").execute();
fail();
} catch (NotImplementedOperationException e) {
assertEquals("HTTP 501 Not Implemented", e.getMessage());
}
}
@SuppressWarnings("deprecation")
@Test
public void testInvalidConformanceCall() {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.conformance();
fail();
} catch (IllegalArgumentException e) {
assertEquals("Must call fetchConformance() instead of conformance() for RI/STU3+ structures", e.getMessage());
}
}
@Test
public void testPutDoesntForceAllIdsJson() throws Exception {
IParser p = ourCtx.newJsonParser();
Patient patient = new Patient();
patient.setId("PATIENT1");
patient.addName().addFamily("PATIENT1");
Bundle bundle = new Bundle();
bundle.setId("BUNDLE1");
bundle.addEntry().setResource(patient);
final String encoded = p.encodeResourceToString(bundle);
assertEquals("{\"resourceType\":\"Bundle\",\"id\":\"BUNDLE1\",\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"PATIENT1\",\"name\":[{\"family\":[\"PATIENT1\"]}]}}]}", encoded);
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(encoded), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
//@formatter:off
client
.update()
.resource(bundle)
.prefer(PreferReturnEnum.REPRESENTATION)
.encodedJson()
.execute();
//@formatter:on
HttpPut httpRequest = (HttpPut) capt.getValue();
assertEquals("http://example.com/fhir/Bundle/BUNDLE1?_format=json", httpRequest.getURI().toASCIIString());
String requestString = IOUtils.toString(httpRequest.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals(encoded, requestString);
}
@Test
public void testResponseHasContentTypeMissing() throws Exception {
IParser p = ourCtx.newJsonParser();
Patient patient = new Patient();
patient.addName().addFamily("FAM");
final String respString = p.encodeResourceToString(patient);
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(null);
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.read().resource(Patient.class).withId("1").execute();
fail();
} catch (NonFhirResponseException e) {
assertEquals("Response contains no Content-Type", e.getMessage());
}
// Patient resp = client.read().resource(Patient.class).withId("1").execute();
// assertEquals("FAM", resp.getNameFirstRep().getFamilyAsSingleString());
}
@Test
public void testResponseHasContentTypeNonFhir() throws Exception {
IParser p = ourCtx.newJsonParser();
Patient patient = new Patient();
patient.addName().addFamily("FAM");
final String respString = p.encodeResourceToString(patient);
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "text/plain"));
// when(myHttpResponse.getEntity().getContentType()).thenReturn(null);
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.read().resource(Patient.class).withId("1").execute();
fail();
} catch (NonFhirResponseException e) {
assertEquals("Response contains non FHIR Content-Type 'text/plain' : {\"resourceType\":\"Patient\",\"name\":[{\"family\":[\"FAM\"]}]}", e.getMessage());
}
// Patient resp = client.read().resource(Patient.class).withId("1").execute();
// assertEquals("FAM", resp.getNameFirstRep().getFamilyAsSingleString());
}
@Test
public void testSearchByDate() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
DateTimeDt now = DateTimeDt.withCurrentTime();
String dateString = now.getValueAsString().substring(0, 10);
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.BIRTHDATE.after().day(dateString))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?birthdate=gt"+dateString + "&_format=json", capt.getAllValues().get(idx).getURI().toString());
idx++;
}
@Test
public void testSearchByString() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.NAME.matches().value("AAA"))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString());
idx++;
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.NAME.matches().value(new StringDt("AAA")))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString());
idx++;
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.NAME.matches().values("AAA", "BBB"))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name=AAA,BBB&_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
idx++;
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.NAME.matches().values(Arrays.asList("AAA", "BBB")))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name=AAA,BBB&_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
idx++;
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.NAME.matchesExactly().value("AAA"))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name%3Aexact=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString());
idx++;
//@formatter:off
client.search()
.forResource("Patient")
.where(Patient.NAME.matchesExactly().value(new StringDt("AAA")))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name%3Aexact=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString());
idx++;
}
@Test
public void testSearchByUrl() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
//@formatter:off
client.search()
.forResource("Device")
.where(Device.URL.matches().value("http://foo.com"))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Device?url=http://foo.com&_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
idx++;
}
@Test
public void testAcceptHeaderWithEncodingSpecified() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
//@formatter:off
client.setEncoding(EncodingEnum.JSON);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
//@formatter:off
client.setEncoding(EncodingEnum.XML);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
}
@Test
public void testSearchForUnknownType() throws Exception {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.search(new UriDt("?aaaa"));
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unable to determine the resource type from the given URI: ?aaaa", e.getMessage());
}
}
@Test
public void testTransactionWithInvalidBody() throws Exception {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
// Transaction
try {
client.transaction().withBundle("FOO");
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage());
}
// Create
try {
client.create().resource("FOO").execute();
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage());
}
// Update
try {
client.update().resource("FOO").execute();
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage());
}
// Validate
try {
client.validate().resource("FOO").execute();
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage());
}
}
//TODO: narratives don't work without stax
@Test
@Disabled
public void testUpdateById() throws Exception {
IParser p = ourCtx.newJsonParser();
OperationOutcome conf = new OperationOutcome();
conf.getText().setDivAsString("OK!");
final String respString = p.encodeResourceToString(conf);
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient pt = new Patient();
pt.setId("222");
pt.getText().setDivAsString("A PATIENT");
client.update().resource(pt).withId("111").execute();
ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString());
assertEquals("http://example.com/fhir/Patient/111", capt.getAllValues().get(0).getURI().toASCIIString());
validateUserAgent(capt);
assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", ""));
assertEquals(Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue());
String body = extractBodyAsString(capt);
assertThat(body, containsString("<id value=\"111\"/>"));
}
// TODO: narratives don't work without stax
@Test
@Disabled
public void testUpdateWithPreferRepresentationServerReturnsOO() throws Exception {
final IParser p = ourCtx.newJsonParser();
final OperationOutcome resp0 = new OperationOutcome();
resp0.getText().setDivAsString("OK!");
final Patient resp1 = new Patient();
resp1.getText().setDivAsString("FINAL VALUE");
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) throws Throwable {
return new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3") };
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
if (myAnswerCount++ == 0) {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp0)), Charset.forName("UTF-8"));
} else {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8"));
}
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient pt = new Patient();
pt.setId("Patient/222");
pt.getText().setDivAsString("A PATIENT");
MethodOutcome outcome = client.update().resource(pt).prefer(PreferReturnEnum.REPRESENTATION).execute();
assertEquals(2, myAnswerCount);
assertNotNull(outcome.getOperationOutcome());
assertNotNull(outcome.getResource());
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">OK!</div>", ((OperationOutcome) outcome.getOperationOutcome()).getText().getDivAsString());
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">FINAL VALUE</div>", ((Patient) outcome.getResource()).getText().getDivAsString());
assertEquals(myAnswerCount, capt.getAllValues().size());
assertEquals("http://example.com/fhir/Patient/222", capt.getAllValues().get(0).getURI().toASCIIString());
assertEquals("http://foo.com/base/Patient/222/_history/3", capt.getAllValues().get(1).getURI().toASCIIString());
}
@Test
public void testUpdateWithPreferRepresentationServerReturnsResource() throws Exception {
final IParser p = ourCtx.newJsonParser();
final Patient resp1 = new Patient();
resp1.setActive(true);
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) throws Throwable {
return new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3") };
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
myAnswerCount++;
return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient pt = new Patient();
pt.setId("Patient/222");
pt.getText().setDivAsString("A PATIENT");
MethodOutcome outcome = client.update().resource(pt).prefer(PreferReturnEnum.REPRESENTATION).execute();
assertEquals(1, myAnswerCount);
assertNull(outcome.getOperationOutcome());
assertNotNull(outcome.getResource());
assertEquals(true, ((Patient) outcome.getResource()).getActive());
assertEquals(myAnswerCount, capt.getAllValues().size());
assertEquals("http://example.com/fhir/Patient/222?_format=json", capt.getAllValues().get(0).getURI().toASCIIString());
}
@Test
public void testUserAgentForConformance() throws Exception {
IParser p = ourCtx.newJsonParser();
Conformance conf = new Conformance();
conf.setCopyright("COPY");
final String respString = p.encodeResourceToString(conf);
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
client.fetchConformance().ofType(Conformance.class).execute();
assertEquals("http://example.com/fhir/metadata?_format=json", capt.getAllValues().get(0).getURI().toASCIIString());
validateUserAgent(capt);
}
// TODO: narratives don't work without stax
@Test
@Disabled
public void testValidate() throws Exception {
final IParser p = ourCtx.newXmlParser();
final OperationOutcome resp0 = new OperationOutcome();
resp0.getText().setDivAsString("OK!");
ArgumentCaptor<Request> capt = ArgumentCaptor.forClass(Request.class);
when(myHttpClient.newCall(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.execute().code()).thenReturn(200);
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) throws Throwable {
return new Header[] {};
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp0)), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient pt = new Patient();
pt.setId("Patient/222");
pt.getText().setDivAsString("A PATIENT");
MethodOutcome outcome = client.validate().resource(pt).execute();
assertNotNull(outcome.getOperationOutcome());
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">OK!</div>", ((OperationOutcome) outcome.getOperationOutcome()).getText().getDivAsString());
}
*/
@AfterAll
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 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 javax.annotation.Nonnull;
import java.util.List;

View File

@ -173,7 +173,7 @@ public class BundleBuilder {
*
* @param theResource The resource to create
*/
public CreateBuilder addCreateEntry(IBaseResource theResource) {
public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) {
setBundleField("type", "transaction");
IBase request = addEntryAndReturnRequest(theResource);
@ -338,7 +338,6 @@ public class BundleBuilder {
setBundleField("type", theType);
}
public static class UpdateBuilder {
private final IPrimitiveType<?> myUrl;

View File

@ -108,6 +108,8 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully delet
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}" for resource type "{1}". Valid search parameters for this search are: {2}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSortParameter=Unknown _sort parameter value "{0}" for resource type "{1}" (Note: sort parameters values must use a valid Search Parameter). Valid values for this search are: {2}
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidBundleTypeForStorage=Unable to store a Bundle resource on this server with a Bundle.type value of: {0}. Note that if you are trying to perform a FHIR 'transaction' or 'batch' operation you should POST the Bundle resource to the Base URL of the server, not to the '/Bundle' endpoint.
ca.uhn.fhir.rest.api.PatchTypeEnum.missingPatchContentType=Missing or invalid content type for PATCH operation
ca.uhn.fhir.rest.api.PatchTypeEnum.invalidPatchContentType=Invalid Content-Type for PATCH operation: {0}
ca.uhn.fhir.jpa.dao.BaseTransactionProcessor.unsupportedResourceType=Resource {0} is not supported on this server. Supported resource types: {1}

View File

@ -683,7 +683,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
OutcomeResponseHandler binding = new OutcomeResponseHandler(myPrefer);
Map<String, List<String>> params = new HashMap<String, List<String>>();
Map<String, List<String>> params = new HashMap<>();
return invoke(params, binding, invocation);
}

View File

@ -42,11 +42,11 @@ import ca.uhn.fhir.util.FhirTerser;
public abstract class RestfulClientFactory implements IRestfulClientFactory {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulClientFactory.class);
private Set<String> myValidatedServerBaseUrls = Collections.synchronizedSet(new HashSet<String>());
private final Set<String> myValidatedServerBaseUrls = Collections.synchronizedSet(new HashSet<>());
private int myConnectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;
private int myConnectTimeout = DEFAULT_CONNECT_TIMEOUT;
private FhirContext myContext;
private Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers = new HashMap<>();
private final Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers = new HashMap<>();
private ServerValidationModeEnum myServerValidationMode = DEFAULT_SERVER_VALIDATION_MODE;
private int mySocketTimeout = DEFAULT_SOCKET_TIMEOUT;
private String myProxyUsername;
@ -316,7 +316,7 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
String serverFhirVersionString = null;
Object value = t.getSingleValueOrNull(conformance, "fhirVersion");
if (value instanceof IPrimitiveType) {
serverFhirVersionString = IPrimitiveType.class.cast(value).getValueAsString();
serverFhirVersionString = ((IPrimitiveType<?>) value).getValueAsString();
}
FhirVersionEnum serverFhirVersionEnum = null;
if (StringUtils.isBlank(serverFhirVersionString)) {

View File

@ -84,7 +84,7 @@ public class BundleBuilderExamples {
patient.setActive(true);
// Add the patient as a create (aka POST) to the Bundle
builder.addCreateEntry(patient);
builder.addTransactionCreateEntry(patient);
// Execute the transaction
IBaseBundle outcome = myFhirClient.transaction().withBundle(builder.getBundle()).execute();
@ -102,7 +102,7 @@ public class BundleBuilderExamples {
patient.addIdentifier().setSystem("http://foo").setValue("bar");
// Add the patient as a create (aka POST) to the Bundle
builder.addCreateEntry(patient).conditional("Patient?identifier=http://foo|bar");
builder.addTransactionCreateEntry(patient).conditional("Patient?identifier=http://foo|bar");
// Execute the transaction
IBaseBundle outcome = myFhirClient.transaction().withBundle(builder.getBundle()).execute();

View File

@ -0,0 +1,8 @@
---
type: add
issue: 2323
title: "The JPA server has a new setting on the `ModelConfig` bean called \"AutoVersionReferencesAtPaths\". Using
this setting, the server can be configured to add the current target resource version ID to any resource
references found in a resource being stored. In addition, a new setting has been added to the JPA ModelConfig
bean that allows `_include` statements to respect versioned references, and actually include the correct
version for the reference."

View File

@ -240,7 +240,7 @@ Use the `$mdm-update-link` operation to change the `matchResult` update of an md
<td>String</td>
<td>1..1</td>
<td>
The id of the source resource.
The id of the target resource.
</td>
</tr>
<tr>
@ -318,6 +318,14 @@ This operation takes the following parameters:
The id of the Golden Resource to merge data into.
</td>
</tr>
<tr>
<td>resource</td>
<td>Resource</td>
<td>0..1</td>
<td>
Optional manually merged Golden Resource. All values except for the metadata, PID and identifiers will be copied from this resource, if it is present. If no value is specified, all fields from the resource pointed to by "fromGoldenResourceId" will be copied instead.
</td>
</tr>
</tbody>
</table>

View File

@ -356,6 +356,14 @@ The following algorithms are currently supported:
</td>
<td>If an optional "identifierSystem" is provided, then the identifiers only match when they belong to that system</td>
</tr>
<tr>
<td>EMPTY_FIELD</td>
<td>matcher</td>
<td>
Matches an empty field.
</td>
<td></td>
</tr>
<tr>
<td>JARO_WINKLER</td>
<td>similarity</td>
@ -395,7 +403,7 @@ The following algorithms are currently supported:
<a href="https://github.com/tdebatty/java-string-similarity#sorensen-dice-coefficient">tdebatty Sorensen-Dice coefficient</a>
</td>
<td></td>
</tr>
</tr>
</tbody>
</table>

View File

@ -265,9 +265,11 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/
DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest);
// /**
// * Invoke the everything operation
// */
// IBundleProvider everything(IIdType theId);
/**
* Returns the current version ID for the given resource
*/
default String getCurrentVersionId(IIdType theReferenceElement) {
return read(theReferenceElement.toVersionless()).getIdElement().getVersionIdPart();
}
}

View File

@ -47,8 +47,9 @@ public class DaoMethodOutcome extends MethodOutcome {
/**
* Was this a NO-OP - Typically because of an update to a resource that already matched the contents provided
*/
public void setNop(boolean theNop) {
public DaoMethodOutcome setNop(boolean theNop) {
myNop = theNop;
return this;
}
public IBasePersistedResource getEntity() {

View File

@ -120,6 +120,7 @@ import javax.annotation.PostConstruct;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@ -259,6 +260,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource);
preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
@ -274,7 +276,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity = myEntityManager.find(ResourceTable.class, pid.getId());
IBaseResource resource = toResource(entity, false);
theResource.setId(resource.getIdElement().getValue());
return toMethodOutcome(theRequest, entity, resource).setCreated(false);
return toMethodOutcome(theRequest, entity, resource).setCreated(false).setNop(true);
}
}
@ -1129,6 +1131,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return readEntity(theId, true, theRequest);
}
@Override
@Transactional
public String getCurrentVersionId(IIdType theReferenceElement) {
return Long.toString(readEntity(theReferenceElement.toVersionless(), null).getVersion());
}
@Override
@Transactional
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) {
@ -1459,6 +1467,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resource = theResource;
preProcessResourceForStorage(resource);
preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
final ResourceTable entity;
@ -1529,6 +1538,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resource.setId(entity.getIdDt().getValue());
DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, resource).setCreated(wasDeleted);
outcome.setPreviousResource(oldResource);
if (!outcome.isNop()) {
outcome.setId(outcome.getId().withVersion(Long.toString(outcome.getId().getVersionIdPartAsLong() + 1)));
}
return outcome;
}

View File

@ -27,8 +27,11 @@ import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
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.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
@ -53,11 +56,10 @@ import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.InstantType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -67,6 +69,7 @@ import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -77,30 +80,85 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseStorageDao {
private static final Logger ourLog = LoggerFactory.getLogger(BaseStorageDao.class);
@Autowired
protected ISearchParamRegistry mySearchParamRegistry;
@Autowired
protected FhirContext myFhirContext;
@Autowired
protected DaoRegistry myDaoRegistry;
@Autowired
protected ModelConfig myModelConfig;
/**
* May be overridden by subclasses to validate resources prior to storage
*
* @param theResource The resource that is about to be stored
* @deprecated Use {@link #preProcessResourceForStorage(IBaseResource, RequestDetails, TransactionDetails, boolean)} instead
*/
protected void preProcessResourceForStorage(IBaseResource theResource) {
// nothing
}
/**
* May be overridden by subclasses to validate resources prior to storage
*
* @param theResource The resource that is about to be stored
* @since 5.3.0
*/
protected void preProcessResourceForStorage(IBaseResource theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean thePerformIndexing) {
verifyResourceTypeIsAppropriateForDao(theResource);
verifyResourceIdIsValid(theResource);
verifyBundleTypeIsAppropriateForStorage(theResource);
replaceAbsoluteReferencesWithRelative(theResource);
performAutoVersioning(theResource, thePerformIndexing);
}
/**
* Sanity check - Is this resource the right type for this DAO?
*/
private void verifyResourceTypeIsAppropriateForDao(IBaseResource theResource) {
String type = getContext().getResourceType(theResource);
if (getResourceName() != null && !getResourceName().equals(type)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "incorrectResourceType", type, getResourceName()));
}
}
/**
* Verify that the resource ID is actually valid according to FHIR's rules
*/
private void verifyResourceIdIsValid(IBaseResource theResource) {
if (theResource.getIdElement().hasIdPart()) {
if (!theResource.getIdElement().isIdPartValid()) {
throw new InvalidRequestException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart()));
}
}
}
/*
* Replace absolute references with relative ones if configured to do so
*/
/**
* Verify that we're not storing a Bundle with a disallowed bundle type
*/
private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) {
if (theResource instanceof IBaseBundle) {
Set<String> allowedBundleTypes = getConfig().getBundleTypesAllowedForStorage();
String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource);
bundleType = defaultString(bundleType);
if (!allowedBundleTypes.contains(bundleType)) {
String message = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidBundleTypeForStorage", (isNotBlank(bundleType) ? bundleType : "(missing)"));
throw new UnprocessableEntityException(message);
}
}
}
/**
* Replace absolute references with relative ones if configured to do so
*/
private void replaceAbsoluteReferencesWithRelative(IBaseResource theResource) {
if (getConfig().getTreatBaseUrlsAsLocal().isEmpty() == false) {
FhirTerser t = getContext().newTerser();
List<ResourceReferenceInfo> refs = t.getAllResourceReferences(theResource);
@ -114,17 +172,38 @@ public abstract class BaseStorageDao {
}
}
}
}
if ("Bundle".equals(type)) {
Set<String> allowedBundleTypes = getConfig().getBundleTypesAllowedForStorage();
String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource);
bundleType = defaultString(bundleType);
if (!allowedBundleTypes.contains(bundleType)) {
String message = "Unable to store a Bundle resource on this server with a Bundle.type value of: " + (isNotBlank(bundleType) ? bundleType : "(missing)");
throw new UnprocessableEntityException(message);
/**
* Handle {@link ModelConfig#getAutoVersionReferenceAtPaths() auto-populate-versions}
*
* We only do this if thePerformIndexing is true because if it's false, that means
* we're in a FHIR transaction during the first phase of write operation processing,
* meaning that the versions of other resources may not have need updated yet. For example
* we're about to store an Observation with a reference to a Patient, and that Patient
* is also being updated in the same transaction, during the first "no index" phase,
* the Patient will not yet have its version number incremented, so it would be wrong
* to use that value. During the second phase it is correct.
*
* Also note that {@link BaseTransactionProcessor} also has code to do auto-versioning
* and it is the one that takes care of the placeholder IDs. Look for the other caller of
* {@link #extractReferencesToAutoVersion(FhirContext, ModelConfig, IBaseResource)}
* to find this.
*/
private void performAutoVersioning(IBaseResource theResource, boolean thePerformIndexing) {
if (thePerformIndexing) {
Set<IBaseReference> referencesToVersion = extractReferencesToAutoVersion(myFhirContext, myModelConfig, theResource);
for (IBaseReference nextReference : referencesToVersion) {
IIdType referenceElement = nextReference.getReferenceElement();
if (!referenceElement.hasBaseUrl()) {
String resourceType = referenceElement.getResourceType();
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
String targetVersionId = dao.getCurrentVersionId(referenceElement);
String newTargetReference = referenceElement.withVersion(targetVersionId).getValue();
nextReference.setReference(newTargetReference);
}
}
}
}
protected DaoMethodOutcome toMethodOutcome(RequestDetails theRequest, @Nonnull final IBasePersistedResource theEntity, @Nonnull IBaseResource theResource) {
@ -267,4 +346,28 @@ public abstract class BaseStorageDao {
}
}
/**
* @see ModelConfig#getAutoVersionReferenceAtPaths()
*/
@Nonnull
public static Set<IBaseReference> extractReferencesToAutoVersion(FhirContext theFhirContext, ModelConfig theModelConfig, IBaseResource theResource) {
Map<IBaseReference, Object> references = Collections.emptyMap();
if (!theModelConfig.getAutoVersionReferenceAtPaths().isEmpty()) {
String resourceName = theFhirContext.getResourceType(theResource);
for (String nextPath : theModelConfig.getAutoVersionReferenceAtPathsByResourceType(resourceName)) {
List<IBaseReference> nextReferences = theFhirContext.newTerser().getValues(theResource, nextPath, IBaseReference.class);
for (IBaseReference next : nextReferences) {
if (next.getReferenceElement().hasVersionIdPart()) {
continue;
}
if (references.isEmpty()) {
references = new IdentityHashMap<>();
}
references.put(next, null);
}
}
}
return references.keySet();
}
}

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
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.jpa.util.JpaInterceptorBroadcaster;
@ -78,6 +79,7 @@ import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -130,10 +132,12 @@ public abstract class BaseTransactionProcessor {
private HapiTransactionService myHapiTransactionService;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private ModelConfig myModelConfig;
@PostConstruct
public void start() {
ourLog.trace("Starting transaction processor");
}
public <BUNDLE extends IBaseBundle> BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
@ -195,7 +199,7 @@ public abstract class BaseTransactionProcessor {
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome,
IBase newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) {
IIdType newId = outcome.getId().toUnqualifiedVersionless();
IIdType newId = outcome.getId().toUnqualified();
IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
if (newId.equals(resourceId) == false) {
idSubstitutions.put(resourceId, newId);
@ -900,20 +904,32 @@ public abstract class BaseTransactionProcessor {
}
// References
Set<IBaseReference> referencesToVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource);
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
for (ResourceReferenceInfo nextRef : allRefs) {
IIdType nextId = nextRef.getResourceReference().getReferenceElement();
IBaseReference resourceReference = nextRef.getResourceReference();
IIdType nextId = resourceReference.getReferenceElement();
if (!nextId.hasIdPart()) {
continue;
}
if (theIdSubstitutions.containsKey(nextId)) {
IIdType newId = theIdSubstitutions.get(nextId);
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
nextRef.getResourceReference().setReference(newId.getValue());
if (referencesToVersion.contains(resourceReference)) {
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(newId);
resourceReference.setReference(newId.getValue());
} else {
resourceReference.setReference(newId.toVersionless().getValue());
}
} else if (nextId.getValue().startsWith("urn:")) {
throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
} else {
ourLog.debug(" * Reference [{}] does not exist in bundle", nextId);
if (referencesToVersion.contains(resourceReference)) {
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId);
if (!outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
resourceReference.setReference(nextId.getValue());
}
}
}
}
@ -928,7 +944,7 @@ public abstract class BaseTransactionProcessor {
if (theIdSubstitutions.containsKey(nextUriString)) {
IIdType newId = theIdSubstitutions.get(nextUriString);
ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
nextRef.setValueAsString(newId.getValue());
nextRef.setValueAsString(newId.toVersionless().getValue());
} else {
ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
}

View File

@ -22,6 +22,8 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -32,8 +34,8 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
public class FhirResourceDaoBundleDstu2 extends BaseHapiFhirResourceDao<Bundle> {
@Override
protected void preProcessResourceForStorage(IBaseResource theResource) {
super.preProcessResourceForStorage(theResource);
protected void preProcessResourceForStorage(IBaseResource theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean thePerformIndexing) {
super.preProcessResourceForStorage(theResource, theRequestDetails, theTransactionDetails, thePerformIndexing);
for (Entry next : ((Bundle)theResource).getEntry()) {
next.setFullUrl((String) null);

View File

@ -436,7 +436,13 @@ public class IdHelperService {
@Nonnull
public Long getPidOrThrowException(IAnyResource theResource) {
return (Long) theResource.getUserData(RESOURCE_PID);
Long retVal = (Long) theResource.getUserData(RESOURCE_PID);
if (retVal == null) {
throw new IllegalStateException(
String.format("Unable to find %s in the user data for %s with ID %s", RESOURCE_PID, theResource, theResource.getId())
);
}
return retVal;
}
public IIdType resourceIdFromPidOrThrowException(Long thePid) {

View File

@ -29,7 +29,9 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
@ -42,6 +44,8 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
@ -87,6 +91,7 @@ import com.healthmarketscience.sqlbuilder.Condition;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -147,6 +152,8 @@ public class SearchBuilder implements ISearchBuilder {
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired
private FhirContext myContext;
@ -174,8 +181,11 @@ public class SearchBuilder implements ISearchBuilder {
private SqlObjectFactory mySqlBuilderFactory;
@Autowired
private HibernatePropertiesProvider myDialectProvider;
@Autowired
private ModelConfig myModelConfig;
private boolean hasNextIteratorQuery = false;
/**
* Constructor
*/
@ -428,7 +438,7 @@ public class SearchBuilder implements ISearchBuilder {
//-- exclude the pids already in the previous iterator
if (hasNextIteratorQuery)
sqlBuilder.excludeResourceIdsPredicate(myPidSet);
/*
* Sort
*
@ -532,23 +542,23 @@ public class SearchBuilder implements ISearchBuilder {
break;
case QUANTITY:
theQueryStack.addSortOnQuantity(myResourceName, theSort.getParamName(), ascending);
break;
break;
case COMPOSITE:
List<RuntimeSearchParam> compositList = param.getCompositeOf();
if (compositList == null) {
throw new InvalidRequestException("The composite _sort parameter " + theSort.getParamName() + " is not defined by the resource " + myResourceName);
}
if (compositList.size() != 2) {
throw new InvalidRequestException("The composite _sort parameter " + theSort.getParamName()
+ " must have 2 composite types declared in parameter annotation, found "
+ compositList.size());
throw new InvalidRequestException("The composite _sort parameter " + theSort.getParamName()
+ " must have 2 composite types declared in parameter annotation, found "
+ compositList.size());
}
RuntimeSearchParam left = compositList.get(0);
RuntimeSearchParam right = compositList.get(1);
createCompositeSort(theQueryStack, myResourceName, left.getParamType(), left.getName(), ascending);
createCompositeSort(theQueryStack, myResourceName, right.getParamType(), right.getName(), ascending);
break;
case SPECIAL:
case HAS:
@ -562,24 +572,30 @@ public class SearchBuilder implements ISearchBuilder {
createSort(theQueryStack, theSort.getChain());
}
private void createCompositeSort(QueryStack theQueryStack, String theResourceName, RestSearchParameterTypeEnum theParamType, String theParamName, boolean theAscending) {
switch (theParamType) {
case STRING:
theQueryStack.addSortOnString(myResourceName, theParamName, theAscending);
break;
case DATE:
theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending);
break;
case TOKEN:
theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending);
break;
case QUANTITY:
theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending);
break;
default:
throw new InvalidRequestException("Don't know how to handle composite parameter with type of " + theParamType + " on _sort="+ theParamName);
case STRING:
theQueryStack.addSortOnString(myResourceName, theParamName, theAscending);
break;
case DATE:
theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending);
break;
case TOKEN:
theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending);
break;
case QUANTITY:
theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending);
break;
case NUMBER:
case REFERENCE:
case COMPOSITE:
case URI:
case HAS:
case SPECIAL:
default:
throw new InvalidRequestException("Don't know how to handle composite parameter with type of " + theParamType + " on _sort=" + theParamName);
}
}
@ -587,34 +603,61 @@ public class SearchBuilder implements ISearchBuilder {
private void doLoadPids(Collection<ResourcePersistentId> thePids, Collection<ResourcePersistentId> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation,
Map<ResourcePersistentId, Integer> thePosition) {
List<Long> myLongPersistentIds;
if (thePids.size() < getMaximumPageSize()) {
myLongPersistentIds = normalizeIdListForLastNInClause(ResourcePersistentId.toLongList(thePids));
} else {
myLongPersistentIds = ResourcePersistentId.toLongList(thePids);
Map<Long, Long> resourcePidToVersion = null;
for (ResourcePersistentId next : thePids) {
if (next.getVersion() != null && myModelConfig.isRespectVersionsForSearchIncludes()) {
if (resourcePidToVersion == null) {
resourcePidToVersion = new HashMap<>();
}
resourcePidToVersion.put(next.getIdAsLong(), next.getVersion());
}
}
List<Long> versionlessPids = ResourcePersistentId.toLongList(thePids);
if (versionlessPids.size() < getMaximumPageSize()) {
versionlessPids = normalizeIdListForLastNInClause(versionlessPids);
}
// -- get the resource from the searchView
Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(myLongPersistentIds);
Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(versionlessPids);
//-- preload all tags with tag definition if any
Map<ResourcePersistentId, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
ResourcePersistentId resourceId;
for (ResourceSearchView next : resourceSearchViewList) {
for (IBaseResourceEntity next : resourceSearchViewList) {
if (next.getDeleted() != null) {
continue;
}
Class<? extends IBaseResource> resourceType = myContext.getResourceDefinition(next.getResourceType()).getImplementingClass();
resourceId = new ResourcePersistentId(next.getId());
ResourcePersistentId resourceId = new ResourcePersistentId(next.getResourceId());
IBaseResource resource = myCallingDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation);
/*
* If a specific version is requested via an include, we'll replace the current version
* with the specific desired version. This is not the most efficient thing, given that
* we're loading the current version and then turning around and throwing it away again.
* This could be optimized and probably should be, but it's not critical given that
* this only applies to includes, which don't tend to be massive in numbers.
*/
if (resourcePidToVersion != null) {
Long version = resourcePidToVersion.get(next.getResourceId());
if (version != null && !version.equals(next.getVersion())) {
resourceId.setVersion(version);
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceType);
next = dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null);
}
}
IBaseResource resource = null;
if (next != null) {
resource = myCallingDao.toResource(resourceType, next, tagMap.get(next.getId()), theForHistoryOperation);
}
if (resource == null) {
ourLog.warn("Unable to find resource {}/{}/_history/{} in database", next.getResourceType(), next.getIdDt().getIdPart(), next.getVersion());
continue;
}
Integer index = thePosition.get(resourceId);
if (index == null) {
ourLog.warn("Got back unexpected resource PID {}", resourceId);
@ -639,17 +682,17 @@ public class SearchBuilder implements ISearchBuilder {
}
}
private Map<ResourcePersistentId, Collection<ResourceTag>> getResourceTagMap(Collection<ResourceSearchView> theResourceSearchViewList) {
private Map<Long, Collection<ResourceTag>> getResourceTagMap(Collection<? extends IBaseResourceEntity> theResourceSearchViewList) {
List<Long> idList = new ArrayList<>(theResourceSearchViewList.size());
//-- find all resource has tags
for (ResourceSearchView resource : theResourceSearchViewList) {
for (IBaseResourceEntity resource : theResourceSearchViewList) {
if (resource.isHasTags())
idList.add(resource.getId());
}
Map<ResourcePersistentId, Collection<ResourceTag>> tagMap = new HashMap<>();
Map<Long, Collection<ResourceTag>> tagMap = new HashMap<>();
//-- no tags
if (idList.size() == 0)
@ -664,11 +707,11 @@ public class SearchBuilder implements ISearchBuilder {
for (ResourceTag tag : tagList) {
resourceId = new ResourcePersistentId(tag.getResourceId());
tagCol = tagMap.get(resourceId);
tagCol = tagMap.get(resourceId.getIdAsLong());
if (tagCol == null) {
tagCol = new ArrayList<>();
tagCol.add(tag);
tagMap.put(resourceId, tagCol);
tagMap.put(resourceId.getIdAsLong(), tagCol);
} else {
tagCol.add(tag);
}
@ -712,8 +755,12 @@ public class SearchBuilder implements ISearchBuilder {
if (theRevIncludes == null || theRevIncludes.isEmpty()) {
return new HashSet<>();
}
String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid";
String findFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid";
String searchPidFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid";
String findPidFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid";
String findVersionFieldName = null;
if (!theReverseMode && myModelConfig.isRespectVersionsForSearchIncludes()) {
findVersionFieldName = "myTargetResourceVersion";
}
List<ResourcePersistentId> nextRoundMatches = new ArrayList<>(theMatches);
HashSet<ResourcePersistentId> allAdded = new HashSet<>();
@ -737,24 +784,37 @@ public class SearchBuilder implements ISearchBuilder {
boolean matchAll = "*".equals(nextInclude.getValue());
if (matchAll) {
String sql;
sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) ";
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT r.").append(findPidFieldName);
if (findVersionFieldName != null) {
sqlBuilder.append(", r." + findVersionFieldName);
}
sqlBuilder.append(" FROM ResourceLink r WHERE r.");
sqlBuilder.append(searchPidFieldName);
sqlBuilder.append(" IN (:target_pids)");
String sql = sqlBuilder.toString();
List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<ResourcePersistentId> nextPartition : partitions) {
TypedQuery<Long> q = theEntityManager.createQuery(sql, Long.class);
TypedQuery<?> q = theEntityManager.createQuery(sql, Object[].class);
q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
List<Long> results = q.getResultList();
for (Long resourceLink : results) {
if (resourceLink == null) {
List<?> results = q.getResultList();
for (Object nextRow : results) {
if (nextRow == null) {
// This can happen if there are outgoing references which are canonical or point to
// other servers
continue;
}
if (theReverseMode) {
pidsToInclude.add(new ResourcePersistentId(resourceLink));
Long resourceLink;
Long version = null;
if (findVersionFieldName != null) {
resourceLink = (Long) ((Object[]) nextRow)[0];
version = (Long) ((Object[]) nextRow)[1];
} else {
pidsToInclude.add(new ResourcePersistentId(resourceLink));
resourceLink = (Long)nextRow;
}
pidsToInclude.add(new ResourcePersistentId(resourceLink, version));
}
}
} else {
@ -789,17 +849,22 @@ public class SearchBuilder implements ISearchBuilder {
String sql;
boolean haveTargetTypesDefinedByParam = param.hasTargets();
String fieldsToLoad = "r." + findPidFieldName;
if (findVersionFieldName != null) {
fieldsToLoad += ", r." + findVersionFieldName;
}
if (targetResourceType != null) {
sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type";
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type";
} else if (haveTargetTypesDefinedByParam) {
sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)";
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)";
} else {
sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)";
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids)";
}
List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<ResourcePersistentId> nextPartition : partitions) {
TypedQuery<Long> q = theEntityManager.createQuery(sql, Long.class);
TypedQuery<?> q = theEntityManager.createQuery(sql, Object[].class);
q.setParameter("src_path", nextPath);
q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
if (targetResourceType != null) {
@ -807,10 +872,18 @@ public class SearchBuilder implements ISearchBuilder {
} else if (haveTargetTypesDefinedByParam) {
q.setParameter("target_resource_types", param.getTargets());
}
List<Long> results = q.getResultList();
for (Long resourceLink : results) {
List<?> results = q.getResultList();
for (Object resourceLink : results) {
if (resourceLink != null) {
pidsToInclude.add(new ResourcePersistentId(resourceLink));
ResourcePersistentId persistentId;
if (findVersionFieldName != null) {
persistentId = new ResourcePersistentId(((Object[])resourceLink)[0]);
persistentId.setVersion((Long) ((Object[])resourceLink)[1]);
} else {
persistentId = new ResourcePersistentId(resourceLink);
}
assert persistentId.getId() instanceof Long;
pidsToInclude.add(persistentId);
}
}
}
@ -1051,6 +1124,7 @@ public class SearchBuilder implements ISearchBuilder {
private final boolean myHaveRawSqlHooks;
private final boolean myHavePerfTraceFoundIdHook;
private final SortSpec mySort;
private final Integer myOffset;
private boolean myFirst = true;
private IncludesIterator myIncludesIterator;
private ResourcePersistentId myNext;
@ -1059,8 +1133,6 @@ public class SearchBuilder implements ISearchBuilder {
private boolean myStillNeedToFetchIncludes;
private int mySkipCount = 0;
private int myNonSkipCount = 0;
private final Integer myOffset;
private ArrayList<SearchQueryExecutor> myQueryList = new ArrayList<>();
private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {
@ -1249,7 +1321,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myNext == null) {
fetchNext();
}
return !NO_MORE.equals(myNext);
return !NO_MORE.equals(myNext);
}
@Override

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
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.rest.server.exceptions.InvalidRequestException;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
@ -50,6 +51,9 @@ public class TransactionProcessorTest {
private MatchResourceUrlService myMatchResourceUrlService;
@MockBean
private HapiTransactionService myHapiTransactionService;
@MockBean
private ModelConfig myModelConfig;
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
private SessionImpl mySession;

View File

@ -389,7 +389,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
myBundleDao.create(bundle, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing)", e.getMessage());
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing). Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage());
}
bundle = new Bundle();
@ -399,7 +399,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
myBundleDao.create(bundle, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: batch-response", e.getMessage());
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: batch-response. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage());
}
bundle = new Bundle();

View File

@ -4,8 +4,6 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.rp.dstu3.PatientResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.TestUtil;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import javax.servlet.ServletConfig;

View File

@ -526,7 +526,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
myBundleDao.create(bundle, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing)", e.getMessage());
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing). Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage());
}
bundle = new Bundle();
@ -536,7 +536,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
myBundleDao.create(bundle, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset", e.getMessage());
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage());
}
bundle = new Bundle();

View File

@ -2985,7 +2985,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
dr.addPresentedForm(attachment);
Attachment attachment2 = new Attachment();
attachment2.setUrl(IdType.newRandomUuid().getValue()); // this one has no subscitution
attachment2.setUrl(IdType.newRandomUuid().getValue()); // this one has no substitution
dr.addPresentedForm(attachment2);
Bundle transactionBundle = new Bundle();

View File

@ -21,6 +21,7 @@ public class FhirResourceDaoR4ReferentialIntegrityTest extends BaseJpaR4Test {
public void afterResetConfig() {
myDaoConfig.setEnforceReferentialIntegrityOnWrite(new DaoConfig().isEnforceReferentialIntegrityOnWrite());
myDaoConfig.setEnforceReferentialIntegrityOnDelete(new DaoConfig().isEnforceReferentialIntegrityOnDelete());
myDaoConfig.setEnforceReferenceTargetTypes(new DaoConfig().isEnforceReferenceTargetTypes());
}
@Test
@ -38,7 +39,7 @@ public class FhirResourceDaoR4ReferentialIntegrityTest extends BaseJpaR4Test {
}
@Test
public void testCreateUnknownReferenceAllow() throws Exception {
public void testCreateUnknownReferenceAllowed() {
myDaoConfig.setEnforceReferentialIntegrityOnWrite(false);
Patient p = new Patient();
@ -50,6 +51,20 @@ public class FhirResourceDaoR4ReferentialIntegrityTest extends BaseJpaR4Test {
}
@Test
public void testCreateUnknownReferenceAllowed_NumericId() {
myDaoConfig.setEnforceReferentialIntegrityOnWrite(false);
myDaoConfig.setEnforceReferenceTargetTypes(false);
Patient p = new Patient();
p.setManagingOrganization(new Reference("Organization/123"));
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
p = myPatientDao.read(id);
assertEquals("Organization/123", p.getManagingOrganization().getReference());
}
@Test
public void testDeleteFail() throws Exception {
Organization o = new Organization();

View File

@ -842,7 +842,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
myBundleDao.create(bundle, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing)", e.getMessage());
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing). Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage());
}
bundle = new Bundle();
@ -852,7 +852,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
myBundleDao.create(bundle, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset", e.getMessage());
assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage());
}
bundle = new Bundle();

View File

@ -0,0 +1,482 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4VersionedReferenceTest.class);
@AfterEach
public void afterEach() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(true);
myDaoConfig.setDeleteEnabled(new DaoConfig().isDeleteEnabled());
myModelConfig.setRespectVersionsForSearchIncludes(new ModelConfig().isRespectVersionsForSearchIncludes());
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
}
@Test
public void testStoreAndRetrieveVersionedReference() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
Patient p = new Patient();
p.setActive(true);
IIdType patientId = myPatientDao.create(p).getId().toUnqualified();
assertEquals("1", patientId.getVersionIdPart());
assertEquals(null, patientId.getBaseUrl());
String patientIdString = patientId.getValue();
Observation observation = new Observation();
observation.getSubject().setReference(patientIdString);
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Read back
observation = myObservationDao.read(observationId);
assertEquals(patientIdString, observation.getSubject().getReference());
}
@Test
public void testDontOverwriteExistingVersion() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
Patient p = new Patient();
p.setActive(true);
myPatientDao.create(p);
// Update the patient
p.setActive(false);
IIdType patientId = myPatientDao.update(p).getId().toUnqualified();
assertEquals("2", patientId.getVersionIdPart());
assertEquals(null, patientId.getBaseUrl());
Observation observation = new Observation();
observation.getSubject().setReference(patientId.withVersion("1").getValue());
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Read back
observation = myObservationDao.read(observationId);
assertEquals(patientId.withVersion("1").getValue(), observation.getSubject().getReference());
}
@Test
public void testInsertVersionedReferenceAtPath() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
Patient p = new Patient();
p.setActive(true);
IIdType patientId = myPatientDao.create(p).getId().toUnqualified();
assertEquals("1", patientId.getVersionIdPart());
assertEquals(null, patientId.getBaseUrl());
String patientIdString = patientId.getValue();
// Create - put an unversioned reference in the subject
Observation observation = new Observation();
observation.getSubject().setReference(patientId.toVersionless().getValue());
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientIdString, observation.getSubject().getReference());
myCaptureQueriesListener.clear();
// Update - put an unversioned reference in the subject
observation = new Observation();
observation.setId(observationId);
observation.addIdentifier().setSystem("http://foo").setValue("bar");
observation.getSubject().setReference(patientId.toVersionless().getValue());
myObservationDao.update(observation);
// Make sure we're not introducing any extra DB operations
assertEquals(5, myCaptureQueriesListener.logSelectQueries().size());
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientIdString, observation.getSubject().getReference());
}
@Test
public void testInsertVersionedReferenceAtPath_InTransaction_SourceAndTargetBothCreated() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
BundleBuilder builder = new BundleBuilder(myFhirCtx);
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.setActive(true);
builder.addTransactionCreateEntry(patient);
Encounter encounter = new Encounter();
encounter.setId(IdType.newRandomUuid());
encounter.addIdentifier().setSystem("http://baz").setValue("baz");
builder.addTransactionCreateEntry(encounter);
Observation observation = new Observation();
observation.getSubject().setReference(patient.getId()); // versioned
observation.getEncounter().setReference(encounter.getId()); // not versioned
builder.addTransactionCreateEntry(observation);
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType encounterId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
IdType observationId = new IdType(outcome.getEntry().get(2).getResponse().getLocation());
assertTrue(patientId.hasVersionIdPart());
assertTrue(encounterId.hasVersionIdPart());
assertTrue(observationId.hasVersionIdPart());
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientId.getValue(), observation.getSubject().getReference());
assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference());
}
@Test
public void testInsertVersionedReferenceAtPath_InTransaction_TargetConditionalCreatedNop() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
{
// Create patient
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.setActive(true);
myPatientDao.create(patient).getId();
// Update patient to make a second version
patient.setActive(false);
myPatientDao.update(patient);
// Create encounter
Encounter encounter = new Encounter();
encounter.setId(IdType.newRandomUuid());
encounter.addIdentifier().setSystem("http://baz").setValue("baz");
myEncounterDao.create(encounter);
}
BundleBuilder builder = new BundleBuilder(myFhirCtx);
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.setActive(true);
builder.addTransactionCreateEntry(patient).conditional("Patient?active=false");
Encounter encounter = new Encounter();
encounter.setId(IdType.newRandomUuid());
encounter.addIdentifier().setSystem("http://baz").setValue("baz");
builder.addTransactionCreateEntry(encounter).conditional("Encounter?identifier=http://baz|baz");
Observation observation = new Observation();
observation.getSubject().setReference(patient.getId()); // versioned
observation.getEncounter().setReference(encounter.getId()); // not versioned
builder.addTransactionCreateEntry(observation);
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("200 OK", outcome.getEntry().get(1).getResponse().getStatus());
assertEquals("201 Created", outcome.getEntry().get(2).getResponse().getStatus());
IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType encounterId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
IdType observationId = new IdType(outcome.getEntry().get(2).getResponse().getLocation());
assertEquals("2", patientId.getVersionIdPart());
assertEquals("1", encounterId.getVersionIdPart());
assertEquals("1", observationId.getVersionIdPart());
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientId.getValue(), observation.getSubject().getReference());
assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference());
}
@Test
public void testInsertVersionedReferenceAtPath_InTransaction_TargetUpdate() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myDaoConfig.setDeleteEnabled(false);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
{
// Create patient
Patient patient = new Patient();
patient.setId("PATIENT");
patient.setActive(true);
myPatientDao.update(patient).getId();
// Update patient to make a second version
patient.setActive(false);
myPatientDao.update(patient);
}
BundleBuilder builder = new BundleBuilder(myFhirCtx);
Patient patient = new Patient();
patient.setId("Patient/PATIENT");
patient.setActive(true);
builder.addTransactionUpdateEntry(patient);
Observation observation = new Observation();
observation.getSubject().setReference(patient.getId()); // versioned
builder.addTransactionCreateEntry(observation);
myCaptureQueriesListener.clear();
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("3", patientId.getVersionIdPart());
assertEquals("1", observationId.getVersionIdPart());
// Make sure we're not introducing any extra DB operations
assertEquals(3, myCaptureQueriesListener.logSelectQueries().size());
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientId.getValue(), observation.getSubject().getReference());
}
@Test
public void testInsertVersionedReferenceAtPath_InTransaction_TargetUpdateConditional() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
{
// Create patient
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.setActive(true);
myPatientDao.create(patient).getId();
// Update patient to make a second version
patient.setActive(false);
myPatientDao.update(patient);
}
BundleBuilder builder = new BundleBuilder(myFhirCtx);
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.setActive(true);
builder
.addTransactionUpdateEntry(patient)
.conditional("Patient?active=false");
Observation observation = new Observation();
observation.getSubject().setReference(patient.getId()); // versioned
builder.addTransactionCreateEntry(observation);
myCaptureQueriesListener.clear();
Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("3", patientId.getVersionIdPart());
assertEquals("1", observationId.getVersionIdPart());
// Make sure we're not introducing any extra DB operations
assertEquals(4, myCaptureQueriesListener.logSelectQueries().size());
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientId.getValue(), observation.getSubject().getReference());
}
@Test
public void testSearchAndIncludeVersionedReference_Asynchronous() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setRespectVersionsForSearchIncludes(true);
// Create the patient
Patient p = new Patient();
p.addIdentifier().setSystem("http://foo").setValue("1");
myPatientDao.create(p);
// Update the patient
p.getIdentifier().get(0).setValue("2");
IIdType patientId = myPatientDao.update(p).getId().toUnqualified();
assertEquals("2", patientId.getVersionIdPart());
Observation observation = new Observation();
observation.getSubject().setReference(patientId.withVersion("1").getValue());
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Search - Non Synchronous for *
{
IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(IBaseResource.INCLUDE_ALL));
assertEquals(1, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 1);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
}
// Search - Non Synchronous for named include
{
IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(Observation.INCLUDE_PATIENT));
assertEquals(1, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 1);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
}
}
@Test
public void testSearchAndIncludeVersionedReference_Synchronous() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setRespectVersionsForSearchIncludes(true);
// Create the patient
Patient p = new Patient();
p.addIdentifier().setSystem("http://foo").setValue("1");
myPatientDao.create(p);
// Update the patient
p.getIdentifier().get(0).setValue("2");
IIdType patientId = myPatientDao.update(p).getId().toUnqualified();
assertEquals("2", patientId.getVersionIdPart());
Observation observation = new Observation();
observation.getSubject().setReference(patientId.withVersion("1").getValue());
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Search - Non Synchronous for *
{
IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(IBaseResource.INCLUDE_ALL));
assertEquals(2, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 2);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
}
// Search - Non Synchronous for named include
{
IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(Observation.INCLUDE_PATIENT));
assertEquals(2, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 2);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
}
}
@Test
public void testSearchAndIncludeUnersionedReference_Asynchronous() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(true);
myModelConfig.setRespectVersionsForSearchIncludes(true);
// Create the patient
Patient p = new Patient();
p.addIdentifier().setSystem("http://foo").setValue("1");
myPatientDao.create(p);
// Update the patient
p.getIdentifier().get(0).setValue("2");
IIdType patientId = myPatientDao.update(p).getId().toUnqualified();
assertEquals("2", patientId.getVersionIdPart());
Observation observation = new Observation();
observation.getSubject().setReference(patientId.withVersion("1").getValue());
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Search - Non Synchronous for *
{
IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(IBaseResource.INCLUDE_ALL));
assertEquals(1, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 1);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue());
}
// Search - Non Synchronous for named include
{
IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(Observation.INCLUDE_PATIENT));
assertEquals(1, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 1);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue());
}
}
@Test
public void testSearchAndIncludeUnversionedReference_Synchronous() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(true);
myModelConfig.setRespectVersionsForSearchIncludes(true);
// Create the patient
Patient p = new Patient();
p.addIdentifier().setSystem("http://foo").setValue("1");
myPatientDao.create(p);
// Update the patient
p.getIdentifier().get(0).setValue("2");
IIdType patientId = myPatientDao.update(p).getId().toUnqualified();
assertEquals("2", patientId.getVersionIdPart());
Observation observation = new Observation();
observation.getSubject().setReference(patientId.withVersion("1").getValue());
IIdType observationId = myObservationDao.create(observation).getId().toUnqualified();
// Search - Non Synchronous for *
{
IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(IBaseResource.INCLUDE_ALL));
assertEquals(2, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 2);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue());
}
// Search - Non Synchronous for named include
{
IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(Observation.INCLUDE_PATIENT));
assertEquals(2, outcome.sizeOrThrowNpe());
List<IBaseResource> resources = outcome.getResources(0, 2);
assertEquals(2, resources.size());
assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue());
assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue());
}
}
}

View File

@ -257,7 +257,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
client.create().resource(resBody).execute().getId();
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction"));
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint."));
}
}

View File

@ -468,7 +468,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
client.create().resource(resBody).execute().getId();
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction"));
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint."));
}
}

View File

@ -798,7 +798,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
client.create().resource(resBody).execute();
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction"));
assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint."));
}
}

View File

@ -51,7 +51,9 @@ public class MdmQueueConsumerLoader {
public void startListeningToMdmChannel() {
if (myMdmChannel == null) {
ChannelConsumerSettings config = new ChannelConsumerSettings();
config.setConcurrentConsumers(myMdmSettings.getConcurrentConsumers());
myMdmChannel = myChannelFactory.getOrCreateReceiver(IMdmSettings.EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, config);
if (myMdmChannel == null) {
ourLog.error("Unable to create receiver for {}", IMdmSettings.EMPI_CHANNEL_NAME);

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.mdm.config;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.mdm.svc.MdmSurvivorshipSvcImpl;
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
import ca.uhn.fhir.mdm.api.IMdmExpungeSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc;
@ -29,6 +30,7 @@ import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc;
import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc;
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.provider.MdmControllerHelper;
import ca.uhn.fhir.mdm.provider.MdmProviderLoader;
@ -77,6 +79,9 @@ public class MdmConsumerConfig {
return new MdmStorageInterceptor();
}
@Bean
IMdmSurvivorshipService mdmSurvivorshipService() { return new MdmSurvivorshipSvcImpl(); }
@Bean
MdmQueueConsumerLoader mdmQueueConsumerLoader() {
return new MdmQueueConsumerLoader();

View File

@ -20,17 +20,18 @@ package ca.uhn.fhir.jpa.mdm.svc;
* #L%
*/
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.mdm.util.TerserUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
@ -60,13 +61,26 @@ public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc {
@Override
@Transactional
public IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
public IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theMergedResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
Long fromGoldenResourcePid = myIdHelperService.getPidOrThrowException(theFromGoldenResource);
Long toGoldenResourcePid = myIdHelperService.getPidOrThrowException(theToGoldenResource);
String resourceType = theMdmTransactionContext.getResourceType();
//Merge attributes, to be determined when survivorship is solved.
myGoldenResourceHelper.mergeFields(theFromGoldenResource, theToGoldenResource);
if (theMergedResource != null ) {
if (myGoldenResourceHelper.hasIdentifier(theMergedResource)) {
throw new IllegalArgumentException("Manually merged resource can not contain identifiers");
}
myGoldenResourceHelper.mergeIndentifierFields(theFromGoldenResource, theMergedResource, theMdmTransactionContext);
myGoldenResourceHelper.mergeIndentifierFields(theToGoldenResource, theMergedResource, theMdmTransactionContext);
theMergedResource.setId(theToGoldenResource.getId());
theToGoldenResource = (IAnyResource) myMdmResourceDaoSvc.upsertGoldenResource(theMergedResource, resourceType).getResource();
} else {
myGoldenResourceHelper.mergeIndentifierFields(theFromGoldenResource, theToGoldenResource, theMdmTransactionContext);
myGoldenResourceHelper.mergeNonIdentiferFields(theFromGoldenResource, theToGoldenResource, theMdmTransactionContext);
//Save changes to the golden resource
myMdmResourceDaoSvc.upsertGoldenResource(theToGoldenResource, resourceType);
}
//Merge the links from the FROM to the TO resource. Clean up dangling links.
mergeGoldenResourceLinks(theFromGoldenResource, theToGoldenResource, toGoldenResourcePid, theMdmTransactionContext);
@ -83,7 +97,8 @@ public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc {
//Save the deprecated resource.
myMdmResourceDaoSvc.upsertGoldenResource(theFromGoldenResource, resourceType);
log(theMdmTransactionContext, "Merged " + theFromGoldenResource.getIdElement().toVersionless() + " into " + theToGoldenResource.getIdElement().toVersionless());
log(theMdmTransactionContext, "Merged " + theFromGoldenResource.getIdElement().toVersionless()
+ " into " + theToGoldenResource.getIdElement().toVersionless());
return theToGoldenResource;
}

View File

@ -44,6 +44,7 @@ import java.util.stream.Stream;
*/
@Service
public class MdmControllerSvcImpl implements IMdmControllerSvc {
@Autowired
MdmControllerHelper myMdmControllerHelper;
@Autowired
@ -54,14 +55,14 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
IMdmLinkUpdaterSvc myIMdmLinkUpdaterSvc;
@Override
public IAnyResource mergeGoldenResources(String theFromGoldenResourceId, String theToGoldenResourceId, MdmTransactionContext theMdmTransactionContext) {
public IAnyResource mergeGoldenResources(String theFromGoldenResourceId, String theToGoldenResourceId, IAnyResource theManuallyMergedGoldenResource, MdmTransactionContext theMdmTransactionContext) {
IAnyResource fromGoldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId);
IAnyResource toGoldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResourceId);
myMdmControllerHelper.validateMergeResources(fromGoldenResource, toGoldenResource);
myMdmControllerHelper.validateSameVersion(fromGoldenResource, theFromGoldenResourceId);
myMdmControllerHelper.validateSameVersion(toGoldenResource, theToGoldenResourceId);
return myGoldenResourceMergerSvc.mergeGoldenResources(fromGoldenResource, toGoldenResource, theMdmTransactionContext);
return myGoldenResourceMergerSvc.mergeGoldenResources(fromGoldenResource, theManuallyMergedGoldenResource, toGoldenResource, theMdmTransactionContext);
}
@Override

View File

@ -20,19 +20,20 @@ package ca.uhn.fhir.jpa.mdm.svc;
* #L%
*/
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
@ -61,23 +62,28 @@ public class MdmEidUpdateService {
private MdmLinkDaoSvc myMdmLinkDaoSvc;
@Autowired
private IMdmSettings myMdmSettings;
@Autowired
private IMdmSurvivorshipService myMdmSurvivorshipService;
void handleMdmUpdate(IAnyResource theTargetResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) {
MdmUpdateContext updateContext = new MdmUpdateContext(theMatchedGoldenResourceCandidate, theTargetResource);
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
void handleMdmUpdate(IAnyResource theResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) {
MdmUpdateContext updateContext = new MdmUpdateContext(theMatchedGoldenResourceCandidate, theResource);
if (updateContext.isRemainsMatchedToSameGoldenResource()) {
// Copy over any new external EIDs which don't already exist.
// TODO NG - Eventually this call will use terser to clone data in, once the surviorship rules for copying data will be confirmed
// myPersonHelper.updatePersonFromUpdatedEmpiTarget(updateContext.getMatchedPerson(), theResource, theEmpiTransactionContext);
if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) {
//update to patient that uses internal EIDs only.
myMdmLinkSvc.updateLink(updateContext.getMatchedGoldenResource(), theResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
myMdmLinkSvc.updateLink(updateContext.getMatchedGoldenResource(), theTargetResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
} else if (!updateContext.isHasEidsInCommon()) {
handleNoEidsInCommon(theResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext);
handleNoEidsInCommon(theTargetResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext);
}
} else {
//This is a new linking scenario. we have to break the existing link and link to the new Golden Resource. For now, we create duplicate.
//updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources possible duplicates
linkToNewGoldenResourceAndFlagAsDuplicate(theResource, updateContext.getExistingGoldenResource(), updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
linkToNewGoldenResourceAndFlagAsDuplicate(theTargetResource, updateContext.getExistingGoldenResource(), updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
myMdmResourceDaoSvc.upsertGoldenResource(updateContext.getMatchedGoldenResource(), theMdmTransactionContext.getResourceType());
}
}
@ -109,7 +115,7 @@ public class MdmEidUpdateService {
private void createNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext, IAnyResource theOldGoldenResource) {
log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource);
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext);
myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
myMdmLinkSvc.updateLink(newGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.mdm.svc;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
@ -63,6 +64,8 @@ public class MdmLinkUpdaterSvcImpl implements IMdmLinkUpdaterSvc {
IMdmSettings myMdmSettings;
@Autowired
MessageHelper myMessageHelper;
@Autowired
IMdmSurvivorshipService myMdmSurvivorshipService;
@Transactional
@Override
@ -89,6 +92,12 @@ public class MdmLinkUpdaterSvcImpl implements IMdmLinkUpdaterSvc {
mdmLink.setMatchResult(theMatchResult);
mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
myMdmLinkDaoSvc.save(mdmLink);
if (theMatchResult == MdmMatchResultEnum.MATCH) {
// only apply survivorship rules in case of a match
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theSourceResource, theGoldenResource, theMdmContext);
}
myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType());
if (theMatchResult == MdmMatchResultEnum.NO_MATCH) {
// Need to find a new Golden Resource to link this target to

View File

@ -122,7 +122,7 @@ public class MdmMatchLinkSvc {
private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
log(theMdmTransactionContext, String.format("There were no matched candidates for MDM, creating a new %s.", theResource.getIdElement().getResourceType()));
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource);
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext);
// TODO GGG :)
// 1. Get the right helper
// 2. Create source resource for the MDM source
@ -136,13 +136,12 @@ public class MdmMatchLinkSvc {
if (myGoldenResourceHelper.isPotentialDuplicate(golenResource, theSourceResource)) {
log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theSourceResource);
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theSourceResource, theMdmTransactionContext);
myMdmLinkSvc.updateLink(newGoldenResource, theSourceResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
myMdmLinkSvc.updateLink(newGoldenResource, golenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
} else {
if (theGoldenResourceCandidate.isMatch()) {
myGoldenResourceHelper.handleExternalEidAddition(golenResource, theSourceResource, theMdmTransactionContext);
//TODO MDM GGG/NG: eventually we need to add survivorship rules of attributes here. Currently no data is copied over except EIDs.
}
myMdmLinkSvc.updateLink(golenResource, theSourceResource, theGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
}

View File

@ -0,0 +1,57 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2021 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.context.FhirContext;
import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.TerserUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
public class MdmSurvivorshipSvcImpl implements IMdmSurvivorshipService {
@Autowired
private FhirContext myFhirContext;
/**
* Merges two golden resources by overwriting all field values on theGoldenResource param for CREATE_RESOURCE,
* UPDATE_RESOURCE, SUBMIT_RESOURCE_TO_MDM, UPDATE_LINK (when setting to MATCH) and MANUAL_MERGE_GOLDEN_RESOURCES.
* PID, identifiers and meta values are not affected by this operation.
*
* @param theTargetResource Target resource to retrieve fields from
* @param theGoldenResource Golden resource to merge fields into
* @param theMdmTransactionContext Current transaction context
* @param <T>
*/
@Override
public <T extends IBase> void applySurvivorshipRulesToGoldenResource(T theTargetResource, T theGoldenResource, MdmTransactionContext theMdmTransactionContext) {
switch (theMdmTransactionContext.getRestOperation()) {
case MERGE_GOLDEN_RESOURCES:
TerserUtil.mergeFields(myFhirContext, (IBaseResource) theTargetResource, (IBaseResource) theGoldenResource, TerserUtil.EXCLUDE_IDS_AND_META);
break;
default:
TerserUtil.replaceFields(myFhirContext, (IBaseResource) theTargetResource, (IBaseResource) theGoldenResource, TerserUtil.EXCLUDE_IDS_AND_META);
break;
}
}
}

View File

@ -35,12 +35,10 @@ public class MdmProviderBatchR4Test extends BaseLinkR4Test {
protected IAnyResource myGoldenMedication;
protected StringType myGoldenMedicationId;
@Autowired
IInterceptorService myInterceptorService;
PointcutLatch afterMdmLatch = new PointcutLatch(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED);
@BeforeEach
public void before() {
super.before();
@ -58,7 +56,6 @@ public class MdmProviderBatchR4Test extends BaseLinkR4Test {
myGoldenMedication = getGoldenResourceFromTargetResource(myMedication);
myGoldenMedicationId = new StringType(myGoldenMedication.getIdElement().getValue());
myInterceptorService.registerAnonymousInterceptor(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED, afterMdmLatch);
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.mdm.util.TerserUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Patient;
@ -38,19 +39,45 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test {
myToGoldenPatientId = new StringType(myToGoldenPatient.getIdElement().getValue());
}
@Test
public void testMergeWithOverride() {
myFromGoldenPatient.getName().clear();
myFromGoldenPatient.addName().setFamily("Family").addGiven("Given");
myFromGoldenPatient.addCommunication();
myFromGoldenPatient.addExtension();
Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources(myFromGoldenPatientId,
myToGoldenPatientId, myFromGoldenPatient, myRequestDetails);
assertEquals(myFromGoldenPatient.getName().size(), mergedSourcePatient.getName().size());
assertEquals(myFromGoldenPatient.getName().get(0).getNameAsSingleString(), mergedSourcePatient.getName().get(0).getNameAsSingleString());
assertEquals(myFromGoldenPatient.getCommunication().size(), mergedSourcePatient.getCommunication().size());
assertEquals(myFromGoldenPatient.getExtension().size(), mergedSourcePatient.getExtension().size());
Patient toGoldenPatient = myPatientDao.read(myToGoldenPatient.getIdElement().toUnqualifiedVersionless());
assertEquals(toGoldenPatient.getName().size(), mergedSourcePatient.getName().size());
assertEquals(toGoldenPatient.getName().get(0).getNameAsSingleString(), mergedSourcePatient.getName().get(0).getNameAsSingleString());
}
@Test
public void testMerge() {
Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources(myFromGoldenPatientId,
myToGoldenPatientId, myRequestDetails);
myToGoldenPatientId, null, myRequestDetails);
// we do not check setActive anymore - as not all types support that
assertTrue(MdmResourceUtil.isGoldenRecord(mergedSourcePatient));
assertTrue(!MdmResourceUtil.isGoldenRecordRedirected(mergedSourcePatient));
assertTrue(MdmResourceUtil.isGoldenRecord(myFromGoldenPatient));
assertEquals(myToGoldenPatient.getIdElement(), mergedSourcePatient.getIdElement());
assertThat(mergedSourcePatient, is(sameGoldenResourceAs(myToGoldenPatient)));
assertEquals(1, getAllRedirectedGoldenPatients().size());
assertEquals(1, getAllGoldenPatients().size());
Patient fromSourcePatient = myPatientDao.read(myFromGoldenPatient.getIdElement().toUnqualifiedVersionless());
assertThat(fromSourcePatient.getActive(), is(false));
assertTrue(!MdmResourceUtil.isGoldenRecord(fromSourcePatient));
assertTrue(MdmResourceUtil.isGoldenRecordRedirected(fromSourcePatient));
//TODO GGG eventually this will need to check a redirect... this is a hack which doesnt work
@ -67,12 +94,39 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test {
assertEquals(link.getLinkSource(), MdmLinkSourceEnum.MANUAL);
}
@Test
public void testMergeWithManualOverride() {
Patient patient = TerserUtil.clone(myFhirContext, myFromGoldenPatient);
patient.setIdElement(null);
Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources(myFromGoldenPatientId,
myToGoldenPatientId, patient, myRequestDetails);
assertEquals(myToGoldenPatient.getIdElement(), mergedSourcePatient.getIdElement());
assertThat(mergedSourcePatient, is(sameGoldenResourceAs(myToGoldenPatient)));
assertEquals(1, getAllRedirectedGoldenPatients().size());
assertEquals(1, getAllGoldenPatients().size());
Patient fromSourcePatient = myPatientDao.read(myFromGoldenPatient.getIdElement().toUnqualifiedVersionless());
assertTrue(!MdmResourceUtil.isGoldenRecord(fromSourcePatient));
assertTrue(MdmResourceUtil.isGoldenRecordRedirected(fromSourcePatient));
List<MdmLink> links = myMdmLinkDaoSvc.findMdmLinksBySourceResource(myFromGoldenPatient);
assertThat(links, hasSize(1));
MdmLink link = links.get(0);
assertEquals(link.getSourcePid(), myFromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong());
assertEquals(link.getGoldenResourcePid(), myToGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong());
assertEquals(link.getMatchResult(), MdmMatchResultEnum.REDIRECT);
assertEquals(link.getLinkSource(), MdmLinkSourceEnum.MANUAL);
}
@Test
public void testUnmanagedMerge() {
StringType fromGoldenResourceId = new StringType(createPatient().getIdElement().getValue());
StringType toGoldenResourceId = new StringType(createPatient().getIdElement().getValue());
try {
myMdmProvider.mergeGoldenResources(fromGoldenResourceId, toGoldenResourceId, myRequestDetails);
myMdmProvider.mergeGoldenResources(fromGoldenResourceId, toGoldenResourceId, null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("Only MDM managed resources can be merged. MDM managed resources must have the HAPI-MDM tag.", e.getMessage());
@ -82,19 +136,19 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test {
@Test
public void testNullParams() {
try {
myMdmProvider.mergeGoldenResources(null, null, myRequestDetails);
myMdmProvider.mergeGoldenResources(null, null, null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromGoldenResourceId cannot be null", e.getMessage());
}
try {
myMdmProvider.mergeGoldenResources(null, myToGoldenPatientId, myRequestDetails);
myMdmProvider.mergeGoldenResources(null, myToGoldenPatientId, null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromGoldenResourceId cannot be null", e.getMessage());
}
try {
myMdmProvider.mergeGoldenResources(myFromGoldenPatientId, null, myRequestDetails);
myMdmProvider.mergeGoldenResources(myFromGoldenPatientId, null, null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("toGoldenResourceId cannot be null", e.getMessage());
@ -104,28 +158,35 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test {
@Test
public void testBadParams() {
try {
myMdmProvider.mergeGoldenResources(new StringType("Patient/1"), new StringType("Patient/1"), myRequestDetails);
myMdmProvider.mergeGoldenResources(new StringType("Patient/1"), new StringType("Patient/1"), null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromGoldenResourceId must be different from toGoldenResourceId", e.getMessage());
}
try {
myMdmProvider.mergeGoldenResources(new StringType("Patient/abc"), myToGoldenPatientId, myRequestDetails);
myMdmProvider.mergeGoldenResources(new StringType("Patient/abc"), myToGoldenPatientId, null, myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Patient/abc is not known", e.getMessage());
}
try {
myMdmProvider.mergeGoldenResources(new StringType("Organization/abc"), myToGoldenPatientId, myRequestDetails);
myMdmProvider.mergeGoldenResources(new StringType("Patient/abc"), myToGoldenPatientId, null, myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Patient/abc is not known", e.getMessage());
}
try {
myMdmProvider.mergeGoldenResources(new StringType("Organization/abc"), myToGoldenPatientId, null, myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Organization/abc is not known", e.getMessage());
}
try {
myMdmProvider.mergeGoldenResources(myFromGoldenPatientId, new StringType("Patient/abc"), myRequestDetails);
myMdmProvider.mergeGoldenResources(myFromGoldenPatientId, new StringType("Patient/abc"), null, myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Patient/abc is not known", e.getMessage());

View File

@ -1,10 +1,10 @@
package ca.uhn.fhir.jpa.mdm.provider;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.util.MessageHelper;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.r4.model.Patient;
@ -15,8 +15,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
public class MdmProviderUpdateLinkR4Test extends BaseLinkR4Test {

View File

@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -34,8 +35,7 @@ import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.*;
public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test {
@ -101,7 +101,7 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test {
private Patient mergeGoldenPatients() {
assertEquals(0, redirectLinkCount());
Patient retval = (Patient) myGoldenResourceMergerSvc.mergeGoldenResources(myFromGoldenPatient, myToGoldenPatient, createMdmContext());
Patient retval = (Patient) myGoldenResourceMergerSvc.mergeGoldenResources(myFromGoldenPatient, null, myToGoldenPatient, createMdmContext());
assertEquals(1, redirectLinkCount());
return retval;
}
@ -151,25 +151,44 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test {
populatePatient(myFromGoldenPatient);
Patient mergedSourcePatient = mergeGoldenPatients();
// TODO NG - Revisit when rules are ready
// HumanName returnedName = mergedSourcePatient.getNameFirstRep();
// assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString());
// assertEquals(FAMILY_NAME, returnedName.getFamily());
// assertEquals(POSTAL_CODE, mergedSourcePatient.getAddressFirstRep().getPostalCode());
HumanName returnedName = mergedSourcePatient.getNameFirstRep();
assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString());
assertEquals(FAMILY_NAME, returnedName.getFamily());
assertEquals(POSTAL_CODE, mergedSourcePatient.getAddressFirstRep().getPostalCode());
}
@Test
public void emptyFromFullTo() {
myFromGoldenPatient.getName().add(new HumanName().addGiven(BAD_GIVEN_NAME));
populatePatient(myToGoldenPatient);
print(myFromGoldenPatient);
Patient mergedSourcePatient = mergeGoldenPatients();
print(mergedSourcePatient);
HumanName returnedName = mergedSourcePatient.getNameFirstRep();
assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString());
assertEquals(FAMILY_NAME, returnedName.getFamily());
assertEquals(POSTAL_CODE, mergedSourcePatient.getAddressFirstRep().getPostalCode());
}
@Test
public void testManualOverride() {
Patient manuallyMergedPatient = new Patient();
populatePatient(manuallyMergedPatient);
manuallyMergedPatient.getNameFirstRep().setFamily("TestFamily");
manuallyMergedPatient.getNameFirstRep().getGiven().clear();
manuallyMergedPatient.getNameFirstRep().addGiven("TestGiven");
MdmTransactionContext ctx = createMdmContext();
ctx.setRestOperation(MdmTransactionContext.OperationType.MANUAL_MERGE_GOLDEN_RESOURCES);
Patient mergedSourcePatient = (Patient) myGoldenResourceMergerSvc
.mergeGoldenResources(myFromGoldenPatient, manuallyMergedPatient, myToGoldenPatient, ctx);
HumanName returnedName = mergedSourcePatient.getNameFirstRep();
assertEquals("TestGiven TestFamily", returnedName.getNameAsSingleString());
assertEquals(POSTAL_CODE, mergedSourcePatient.getAddressFirstRep().getPostalCode());
}
@Test
public void fromLinkToNoLink() {
createMdmLink(myFromGoldenPatient, myTargetPatient1);
@ -376,39 +395,42 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test {
@Test
public void testMergeNames() {
// TODO NG - Revisit when rules are available
// myFromSourcePatient.addName().addGiven("Jim");
// myFromSourcePatient.getNameFirstRep().addGiven("George");
// assertThat(myFromSourcePatient.getName(), hasSize(1));
// assertThat(myFromSourcePatient.getName().get(0).getGiven(), hasSize(2));
//
// myToSourcePatient.addName().addGiven("Jeff");
// myToSourcePatient.getNameFirstRep().addGiven("George");
// assertThat(myToSourcePatient.getName(), hasSize(1));
// assertThat(myToSourcePatient.getName().get(0).getGiven(), hasSize(2));
//
// Patient mergedSourcePatient = mergeSourcePatients();
// assertThat(mergedSourcePatient.getName(), hasSize(2));
// assertThat(mergedSourcePatient.getName().get(0).getGiven(), hasSize(2));
// assertThat(mergedSourcePatient.getName().get(1).getGiven(), hasSize(2));
myFromGoldenPatient.addName().addGiven("Jim");
myFromGoldenPatient.getNameFirstRep().addGiven("George");
assertThat(myFromGoldenPatient.getName(), hasSize(1));
assertThat(myFromGoldenPatient.getName().get(0).getGiven(), hasSize(2));
myToGoldenPatient.addName().addGiven("Jeff");
myToGoldenPatient.getNameFirstRep().addGiven("George");
assertThat(myToGoldenPatient.getName(), hasSize(1));
assertThat(myToGoldenPatient.getName().get(0).getGiven(), hasSize(2));
Patient mergedSourcePatient = mergeGoldenPatients();
assertThat(mergedSourcePatient.getName(), hasSize(2));
assertThat(mergedSourcePatient.getName().get(0).getGiven(), hasSize(2));
assertThat(mergedSourcePatient.getName().get(1).getGiven(), hasSize(2));
assertThat(mergedSourcePatient.getName().get(0).getNameAsSingleString(), is("Jeff George"));
assertThat(mergedSourcePatient.getName().get(1).getNameAsSingleString(), is("Jim George"));
}
@Test
public void testMergeNamesAllSame() {
// TODO NG - Revisit when rules are available
// myFromSourcePatient.addName().addGiven("Jim");
// myFromSourcePatient.getNameFirstRep().addGiven("George");
// assertThat(myFromSourcePatient.getName(), hasSize(1));
// assertThat(myFromSourcePatient.getName().get(0).getGiven(), hasSize(2));
//
// myToSourcePatient.addName().addGiven("Jim");
// myToSourcePatient.getNameFirstRep().addGiven("George");
// assertThat(myToSourcePatient.getName(), hasSize(1));
// assertThat(myToSourcePatient.getName().get(0).getGiven(), hasSize(2));
//
// mergeSourcePatients();
// assertThat(myToSourcePatient.getName(), hasSize(1));
// assertThat(myToSourcePatient.getName().get(0).getGiven(), hasSize(2));
myFromGoldenPatient.addName().addGiven("Jim");
myFromGoldenPatient.getNameFirstRep().addGiven("George");
assertThat(myFromGoldenPatient.getName(), hasSize(1));
assertThat(myFromGoldenPatient.getName().get(0).getGiven(), hasSize(2));
myToGoldenPatient.addName().addGiven("Jim");
myToGoldenPatient.getNameFirstRep().addGiven("George");
assertThat(myToGoldenPatient.getName(), hasSize(1));
assertThat(myToGoldenPatient.getName().get(0).getGiven(), hasSize(2));
mergeGoldenPatients();
assertThat(myToGoldenPatient.getName(), hasSize(1));
assertThat(myToGoldenPatient.getName().get(0).getGiven(), hasSize(2));
assertThat(myToGoldenPatient.getName().get(0).getNameAsSingleString(), is("Jim George"));
}
@Test
@ -425,11 +447,14 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test {
mergeGoldenPatients();
assertThat(myToGoldenPatient.getIdentifier(), hasSize(4));
assertTrue(myToGoldenPatient.getIdentifier().get(0).equalsDeep(new Identifier().setValue("aaa").setSystem("SYSTEM1")));
assertTrue(myToGoldenPatient.getIdentifier().get(1).equalsDeep(new Identifier().setValue("ccc").setSystem("SYSTEM1")));
assertTrue(myToGoldenPatient.getIdentifier().get(2).equalsDeep(new Identifier().setValue("bbb").setSystem("SYSTEM1")));
assertTrue(myToGoldenPatient.getIdentifier().get(3).equalsDeep(new Identifier().setValue("ccc").setSystem("SYSTEM2")));
}
private MdmLink createMdmLink(Patient theSourcePatient, Patient theTargetPatient) {
//TODO GGG Ensure theis comment can be safely removed
//theSourcePatient.addLink().setTarget(new Reference(theTargetPatient));
return myMdmLinkDaoSvc.createOrUpdateLinkEntity(theSourcePatient, theTargetPatient, POSSIBLE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient"));
}

View File

@ -51,23 +51,16 @@ public class MdmLinkSvcTest extends BaseMdmR4Test {
assertLinkCount(0);
Patient goldenPatient = createGoldenPatient();
IdType sourcePatientId = goldenPatient.getIdElement().toUnqualifiedVersionless();
// TODO NG should be ok to remove - assertEquals(0, goldenPatient.getLink().size());
Patient patient = createPatient();
{
myMdmLinkSvc.updateLink(goldenPatient, patient, POSSIBLE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient"));
assertLinkCount(1);
// TODO NG should be ok to remove
// Patient newSourcePatient = myPatientDao.read(sourcePatientId);
// assertEquals(1, newSourcePatient.getLink().size());
}
{
myMdmLinkSvc.updateLink(goldenPatient, patient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient"));
assertLinkCount(1);
// TODO NG should be ok to remove
// Patient newSourcePatient = myPatientDao.read(sourcePatientId);
// assertEquals(0, newSourcePatient.getLink().size());
}
}
@ -129,7 +122,6 @@ public class MdmLinkSvcTest extends BaseMdmR4Test {
@Test
public void testManualMdmLinksCannotBeModifiedBySystem() {
// Patient goldenPatient = createGoldenPatient(buildJaneSourcePatient());
Patient goldenPatient = createGoldenPatient(buildJanePatient());
Patient patient = createPatient(buildJanePatient());
@ -144,7 +136,6 @@ public class MdmLinkSvcTest extends BaseMdmR4Test {
@Test
public void testAutomaticallyAddedNO_MATCHMdmLinksAreNotAllowed() {
// Patient goldenPatient = createGoldenPatient(buildJaneSourcePatient());
Patient goldenPatient = createGoldenPatient(buildJanePatient());
Patient patient = createPatient(buildJanePatient());
@ -159,7 +150,6 @@ public class MdmLinkSvcTest extends BaseMdmR4Test {
@Test
public void testSyncDoesNotSyncNoMatchLinks() {
// Patient sourcePatient = createGoldenPatient(buildJaneSourcePatient());
Patient goldenPatient = createGoldenPatient(buildJanePatient());
Patient patient1 = createPatient(buildJanePatient());
Patient patient2 = createPatient(buildJanePatient());
@ -171,7 +161,6 @@ public class MdmLinkSvcTest extends BaseMdmR4Test {
List<MdmLink> targets = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(goldenPatient);
assertFalse(targets.isEmpty());
assertEquals(2, targets.size());
// TODO NG - OK? original assertTrue(goldenPatient.hasLink());
//TODO GGG update this test once we decide what has to happen here. There is no more "syncing links"
//assertEquals(patient1.getIdElement().toVersionless().getValue(), sourcePatient.getLinkFirstRep().getTarget().getReference());

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
@ -34,12 +35,7 @@ import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.NO_MATCH;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.POSSIBLE_DUPLICATE;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.POSSIBLE_MATCH;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -183,13 +179,13 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
Optional<MdmLink> mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(patient.getIdElement().getIdPartAsLong());
Patient read = getTargetResourceFromMdmLink(mdmLink.get(), "Patient");
// TODO NG - rules haven't been determined yet revisit once implemented...
// assertThat(read.getNameFirstRep().getFamily(), is(equalTo(patient.getNameFirstRep().getFamily())));
// assertThat(read.getNameFirstRep().getGivenAsSingleString(), is(equalTo(patient.getNameFirstRep().getGivenAsSingleString())));
// assertThat(read.getBirthDateElement().toHumanDisplay(), is(equalTo(patient.getBirthDateElement().toHumanDisplay())));
// assertThat(read.getTelecomFirstRep().getValue(), is(equalTo(patient.getTelecomFirstRep().getValue())));
// assertThat(read.getPhoto().getData(), is(equalTo(patient.getPhotoFirstRep().getData())));
// assertThat(read.getGender(), is(equalTo(patient.getGender())));
assertThat(read.getNameFirstRep().getFamily(), is(equalTo(patient.getNameFirstRep().getFamily())));
assertThat(read.getNameFirstRep().getGivenAsSingleString(), is(equalTo(patient.getNameFirstRep().getGivenAsSingleString())));
assertThat(read.getBirthDateElement().toHumanDisplay(), is(equalTo(patient.getBirthDateElement().toHumanDisplay())));
assertThat(read.getTelecomFirstRep().getValue(), is(equalTo(patient.getTelecomFirstRep().getValue())));
assertThat(read.getPhoto().size(), is(equalTo(patient.getPhoto().size())));
assertThat(read.getPhotoFirstRep().getData(), is(equalTo(patient.getPhotoFirstRep().getData())));
assertThat(read.getGender(), is(equalTo(patient.getGender())));
}
@Test
@ -245,7 +241,6 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
@Test
public void testHavingMultipleEIDsOnIncomingPatientMatchesCorrectly() {
Patient patient1 = buildJanePatient();
addExternalEID(patient1, "id_1");
addExternalEID(patient1, "id_2");
@ -273,7 +268,6 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
List<MdmLink> possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
List<Long> duplicatePids = Stream.of(patient1, patient2)
.map(this::getGoldenResourceFromTargetResource)
.map(myIdHelperService::getPidOrNull)
@ -352,7 +346,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
//In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their
//own individual GoldenResource for the purpose of this test.
IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2);
IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE));
myMdmLinkSvc.updateLink(goldenResource, janePatient2, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient"));
assertThat(janePatient, is(not(sameGoldenResourceAs(janePatient2))));
@ -443,7 +437,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
public void testCreateGoldenResourceFromMdmTarget() {
// Create Use Case #2 - adding patient with no EID
Patient janePatient = buildJanePatient();
Patient janeGoldenResourcePatient = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient);
Patient janeGoldenResourcePatient = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE));
// golden record now contains HAPI-generated EID and HAPI tag
assertTrue(MdmResourceUtil.isMdmManaged(janeGoldenResourcePatient));
@ -479,8 +473,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(janePaulPatient);
HumanName nameFirstRep = sourcePatientFromTarget.getNameFirstRep();
// TODO NG attribute propagation has been removed - revisit once source survivorship rules are defined
// assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul")));
assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul")));
}
@Test
@ -491,8 +484,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul);
// TODO NG - rules haven't been determined yet revisit once implemented...
// assertThat(sourcePatientFromTarget.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE)));
assertThat(sourcePatientFromTarget.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE)));
Patient paul2 = buildPaulPatient();
paul2.setGender(Enumerations.AdministrativeGender.FEMALE);
@ -503,7 +495,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
//Newly matched patients aren't allowed to overwrite GoldenResource Attributes unless they are empty,
// so gender should still be set to male.
Patient paul2GoldenResource = (Patient) getGoldenResourceFromTargetResource(paul2);
// assertThat(paul2GoldenResource.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE)));
assertThat(paul2GoldenResource.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE)));
}
@Test
@ -515,8 +507,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
paul = createPatientAndUpdateLinks(paul);
Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul);
// TODO NG - rules haven't been determined yet revisit once implemented...
// assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(incorrectBirthdate));
assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(incorrectBirthdate));
String correctBirthdate = "1990-06-28";
paul.getBirthDateElement().setValueAsString(correctBirthdate);
@ -524,8 +515,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
paul = updatePatientAndUpdateLinks(paul);
sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul);
// TODO NG - rules haven't been determined yet revisit once implemented...
// assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(equalTo(correctBirthdate)));
assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(equalTo(correctBirthdate)));
assertLinkCount(1);
}
@ -612,6 +602,5 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
List<MdmLink> possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
assertThat(patient3, is(possibleDuplicateOf(patient1)));
}
}

View File

@ -0,0 +1,59 @@
package ca.uhn.fhir.jpa.mdm.svc;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MdmSurvivorshipSvcImplTest extends BaseMdmR4Test {
@Autowired
private IMdmSurvivorshipService myMdmSurvivorshipService;
@Test
public void testRulesOnCreate() {
Patient p1 = buildFrankPatient();
Patient p2 = new Patient();
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(p1, p2, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE));
assertFalse(p2.hasIdElement());
assertTrue(p2.getIdentifier().isEmpty());
assertTrue(p2.getMeta().isEmpty());
assertTrue(p1.getNameFirstRep().equalsDeep(p2.getNameFirstRep()));
assertNull(p2.getBirthDate());
assertEquals(p1.getTelecom().size(), p2.getTelecom().size());
assertTrue(p2.getTelecomFirstRep().equalsDeep(p1.getTelecomFirstRep()));
}
@Test
public void testRulesOnMerge() {
Patient p1 = buildFrankPatient();
String p1Name = p1.getNameFirstRep().getNameAsSingleString();
Patient p2 = buildPaulPatient();
String p2Name = p2.getNameFirstRep().getNameAsSingleString();
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(p1, p2, new MdmTransactionContext(MdmTransactionContext.OperationType.MERGE_GOLDEN_RESOURCES));
assertFalse(p2.hasIdElement());
assertFalse(p2.getIdentifier().isEmpty());
assertTrue(p2.getMeta().isEmpty());
assertEquals(2, p2.getName().size());
assertEquals(p2Name, p2.getName().get(0).getNameAsSingleString());
assertEquals(p1Name, p2.getName().get(1).getNameAsSingleString());
assertNull(p2.getBirthDate());
assertEquals(p1.getTelecom().size(), p1.getTelecom().size());
assertTrue(p2.getTelecomFirstRep().equalsDeep(p1.getTelecomFirstRep()));
}
}

View File

@ -123,6 +123,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
Builder.BuilderWithTableName quantityTable = version.onTable("HFJ_SPIDX_QUANTITY");
quantityTable.modifyColumn("20210116.1", "SP_VALUE").nullable().failureAllowed().withType(ColumnTypeEnum.DOUBLE);
// HFJ_RES_LINK
version.onTable("HFJ_RES_LINK")
.addColumn("20210126.1", "TARGET_RESOURCE_VERSION").nullable().type(ColumnTypeEnum.LONG);
}
protected void init520() {

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.api;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.config;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.cross;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.cross;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.cross;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
@ -20,20 +20,25 @@ package ca.uhn.fhir.jpa.model.entity;
* #L%
*/
import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu2.model.Subscription;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.DateTimeType;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
// TODO: move this to ca.uhn.fhir.jpa.model.config
public class ModelConfig {
@ -90,6 +95,9 @@ public class ModelConfig {
private IPrimitiveType<Date> myPeriodIndexEndOfTime;
private NormalizedQuantitySearchLevel myNormalizedQuantitySearchLevel;
private Set<String> myAutoVersionReferenceAtPaths = Collections.emptySet();
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
private boolean myRespectVersionsForSearchIncludes;
/**
* Constructor
@ -268,7 +276,7 @@ public class ModelConfig {
}
HashSet<String> treatBaseUrlsAsLocal = new HashSet<>();
for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet<String>())) {
for (String next : defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet<String>())) {
while (next.endsWith("/")) {
next = next.substring(0, next.length() - 1);
}
@ -618,6 +626,110 @@ public class ModelConfig {
myNormalizedQuantitySearchLevel = theNormalizedQuantitySearchLevel;
}
/**
* When set with resource paths (e.g. <code>"Observation.subject"</code>), any references found at the given paths
* will automatically have versions appended. The version used will be the current version of the given resource.
*
* @since 5.3.0
*/
public Set<String> getAutoVersionReferenceAtPaths() {
return myAutoVersionReferenceAtPaths;
}
/**
* When set with resource paths (e.g. <code>"Observation.subject"</code>), any references found at the given paths
* will automatically have versions appended. The version used will be the current version of the given resource.
* <p>
* Versions will only be added if the reference does not already have a version, so any versioned references
* supplied by the client will take precedence over the automatic current version.
* </p>
* <p>
* Note that for this setting to be useful, the {@link ParserOptions}
* {@link ParserOptions#getDontStripVersionsFromReferencesAtPaths() DontStripVersionsFromReferencesAtPaths}
* option must also be set.
* </p>
*
* @param thePaths A collection of reference paths for which the versions will be appended automatically
* when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that
* only resource name and field names with dots separating is allowed here (no repetition
* indicators, FluentPath expressions, etc.)
* @since 5.3.0
*/
public void setAutoVersionReferenceAtPaths(String... thePaths) {
Set<String> paths = Collections.emptySet();
if (thePaths != null) {
paths = new HashSet<>(Arrays.asList(thePaths));
}
setAutoVersionReferenceAtPaths(paths);
}
/**
* When set with resource paths (e.g. <code>"Observation.subject"</code>), any references found at the given paths
* will automatically have versions appended. The version used will be the current version of the given resource.
* <p>
* Versions will only be added if the reference does not already have a version, so any versioned references
* supplied by the client will take precedence over the automatic current version.
* </p>
* <p>
* Note that for this setting to be useful, the {@link ParserOptions}
* {@link ParserOptions#getDontStripVersionsFromReferencesAtPaths() DontStripVersionsFromReferencesAtPaths}
* option must also be set
* </p>
*
* @param thePaths A collection of reference paths for which the versions will be appended automatically
* when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that
* only resource name and field names with dots separating is allowed here (no repetition
* indicators, FluentPath expressions, etc.)
* @since 5.3.0
*/
public void setAutoVersionReferenceAtPaths(Set<String> thePaths) {
Set<String> paths = defaultIfNull(thePaths, Collections.emptySet());
Map<String, Set<String>> byType = new HashMap<>();
for (String nextPath : paths) {
int doxIdx = nextPath.indexOf('.');
Validate.isTrue(doxIdx > 0, "Invalid path for auto-version reference at path: %s", nextPath);
String type = nextPath.substring(0, doxIdx);
byType.computeIfAbsent(type, t -> new HashSet<>()).add(nextPath);
}
myAutoVersionReferenceAtPaths = paths;
myTypeToAutoVersionReferenceAtPaths = byType;
}
/**
* Returns a sub-collection of {@link #getAutoVersionReferenceAtPaths()} containing only paths
* for the given resource type.
*
* @since 5.3.0
*/
public Set<String> getAutoVersionReferenceAtPathsByResourceType(String theResourceType) {
Validate.notEmpty(theResourceType, "theResourceType must not be null or empty");
Set<String> retVal = myTypeToAutoVersionReferenceAtPaths.get(theResourceType);
retVal = defaultIfNull(retVal, Collections.emptySet());
return retVal;
}
/**
* Should searches with <code>_include</code> respect versioned references, and pull the specific requested version.
* This may have performance impacts on heavily loaded systems.
*
* @since 5.3.0
*/
public boolean isRespectVersionsForSearchIncludes() {
return myRespectVersionsForSearchIncludes;
}
/**
* Should searches with <code>_include</code> respect versioned references, and pull the specific requested version.
* This may have performance impacts on heavily loaded systems.
*
* @since 5.3.0
*/
public void setRespectVersionsForSearchIncludes(boolean theRespectVersionsForSearchIncludes) {
myRespectVersionsForSearchIncludes = theRespectVersionsForSearchIncludes;
}
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
Validate.notBlank(theUrl, "Base URL must not be null or empty");

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
@ -26,6 +26,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@ -88,12 +89,12 @@ public class ResourceLink extends BaseResourceIndex {
@Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true)
@FullTextField
private String myTargetResourceUrl;
@Column(name = "TARGET_RESOURCE_VERSION", nullable = true)
private Long myTargetResourceVersion;
@FullTextField
@Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3
@Temporal(TemporalType.TIMESTAMP)
private Date myUpdated;
@Transient
private transient String myTargetResourceId;
@ -101,6 +102,14 @@ public class ResourceLink extends BaseResourceIndex {
super();
}
public Long getTargetResourceVersion() {
return myTargetResourceVersion;
}
public void setTargetResourceVersion(Long theTargetResourceVersion) {
myTargetResourceVersion = theTargetResourceVersion;
}
public String getTargetResourceId() {
if (myTargetResourceId == null && myTargetResource != null) {
myTargetResourceId = getTargetResource().getIdDt().getIdPart();
@ -178,10 +187,6 @@ public class ResourceLink extends BaseResourceIndex {
return myTargetResourceUrl;
}
public Long getTargetResourcePid() {
return myTargetResourcePid;
}
public void setTargetResourceUrl(IIdType theTargetResourceUrl) {
Validate.isTrue(theTargetResourceUrl.hasBaseUrl());
Validate.isTrue(theTargetResourceUrl.hasResourceType());
@ -199,6 +204,10 @@ public class ResourceLink extends BaseResourceIndex {
myTargetResourceUrl = theTargetResourceUrl.getValue();
}
public Long getTargetResourcePid() {
return myTargetResourcePid;
}
public void setTargetResourceUrlCanonical(String theTargetResourceUrl) {
Validate.notBlank(theTargetResourceUrl);
@ -279,11 +288,15 @@ public class ResourceLink extends BaseResourceIndex {
return retVal;
}
public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, String theTargetResourceType, Long theTargetResourcePid, String theTargetResourceId, Date theUpdated) {
/**
* @param theTargetResourceVersion This should only be populated if the reference actually had a version
*/
public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, String theTargetResourceType, Long theTargetResourcePid, String theTargetResourceId, Date theUpdated, @Nullable Long theTargetResourceVersion) {
ResourceLink retVal = new ResourceLink();
retVal.setSourcePath(theSourcePath);
retVal.setSourceResource(theSourceResource);
retVal.setTargetResource(theTargetResourceType, theTargetResourcePid, theTargetResourceId);
retVal.setTargetResourceVersion(theTargetResourceVersion);
retVal.setUpdated(theUpdated);
return retVal;
}

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.util;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.util;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.util;
/*
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR Model
* HAPI FHIR JPA Model
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%

View File

@ -162,8 +162,9 @@ public class SearchParameterMap implements Serializable {
return this;
}
public void addInclude(Include theInclude) {
public SearchParameterMap addInclude(Include theInclude) {
getIncludes().add(theInclude);
return this;
}
private void addLastUpdateParam(StringBuilder b, DateParam date) {

View File

@ -268,8 +268,8 @@ public class SearchParamExtractorService {
}
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) {
String targetId = nextId.getIdPart();
if (StringUtils.isBlank(targetId)) {
String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - " + nextId.getValue();
if (theFailOnInvalidReference) {
throw new InvalidRequestException(msg);
@ -279,9 +279,11 @@ public class SearchParamExtractorService {
}
}
ResourcePersistentId resolvedTargetId = theTransactionDetails.getResolvedResourceIds().get(thePathAndRef.getRef().getReferenceElement());
IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
ResourcePersistentId resolvedTargetId = theTransactionDetails.getResolvedResourceId(referenceElement);
ResourceLink resourceLink;
Long targetVersionId = nextId.getVersionIdPartAsLong();
if (resolvedTargetId != null) {
/*
@ -289,7 +291,7 @@ public class SearchParamExtractorService {
* need to resolve it again
*/
myResourceLinkResolver.validateTypeOrThrowException(type);
resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, resolvedTargetId.getIdAsLong(), nextId.getIdPart(), transactionDate);
resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, resolvedTargetId.getIdAsLong(), targetId, transactionDate, targetVersionId);
} else if (theFailOnInvalidReference) {
@ -305,7 +307,7 @@ public class SearchParamExtractorService {
} else {
// Cache the outcome in the current transaction in case there are more references
ResourcePersistentId persistentId = new ResourcePersistentId(resourceLink.getTargetResourcePid());
theTransactionDetails.addResolvedResourceId(thePathAndRef.getRef().getReferenceElement(), persistentId);
theTransactionDetails.addResolvedResourceId(referenceElement, persistentId);
}
} else {
@ -317,7 +319,7 @@ public class SearchParamExtractorService {
ResourceTable target;
target = new ResourceTable();
target.setResourceType(typeString);
resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, null, nextId.getIdPart(), transactionDate);
resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, null, targetId, transactionDate, targetVersionId);
}
@ -346,7 +348,8 @@ public class SearchParamExtractorService {
String targetResourceType = targetResource.getResourceType();
Long targetResourcePid = targetResource.getResourceId();
String targetResourceIdPart = theNextId.getIdPart();
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime);
Long targetVersion = theNextId.getVersionIdPartAsLong();
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion);
}

Some files were not shown because too many files have changed in this diff Show More