Correctly handle JPA search by token with system but no code

This commit is contained in:
James Agnew 2016-03-25 16:20:43 +01:00
parent d69365988f
commit f9fa6265df
6 changed files with 243 additions and 82 deletions

View File

@ -35,17 +35,37 @@ public class TokenClientParam extends BaseClientParam implements IParam {
private String myParamName;
@Override
public String getParamName() {
return myParamName;
}
public TokenClientParam(String theParamName) {
myParamName = theParamName;
}
public IMatches exactly() {
return new IMatches() {
@Override
public ICriterion<TokenClientParam> code(String theCode) {
return new TokenCriterion(getParamName(), null, theCode);
}
@Override
public ICriterion<TokenClientParam> identifier(BaseIdentifierDt theIdentifier) {
return new TokenCriterion(getParamName(), theIdentifier.getSystemElement().getValueAsString(), theIdentifier.getValueElement().getValue());
}
@Override
public ICriterion<TokenClientParam> identifier(String theIdentifier) {
return new TokenCriterion(getParamName(), null, theIdentifier);
}
@Override
public ICriterion<TokenClientParam> identifiers(BaseIdentifierDt... theIdentifiers) {
return new TokenCriterion(getParamName(), Arrays.asList(theIdentifiers));
}
@Override
public ICriterion<TokenClientParam> identifiers(List<BaseIdentifierDt> theIdentifiers) {
return new TokenCriterion(getParamName(), theIdentifiers);
}
@Override
public ICriterion<TokenClientParam> systemAndCode(String theSystem, String theCode) {
return new TokenCriterion(getParamName(), defaultString(theSystem), theCode);
@ -55,35 +75,74 @@ public class TokenClientParam extends BaseClientParam implements IParam {
public ICriterion<TokenClientParam> systemAndIdentifier(String theSystem, String theCode) {
return new TokenCriterion(getParamName(), defaultString(theSystem), theCode);
}
@Override
public ICriterion<TokenClientParam> code(String theCode) {
return new TokenCriterion(getParamName(), null, theCode);
}
@Override
public ICriterion<TokenClientParam> identifier(String theIdentifier) {
return new TokenCriterion(getParamName(), null, theIdentifier);
}
@Override
public ICriterion<TokenClientParam> identifier(BaseIdentifierDt theIdentifier) {
return new TokenCriterion(getParamName(), theIdentifier.getSystemElement().getValueAsString(), theIdentifier.getValueElement().getValue());
}
@Override
public ICriterion<TokenClientParam> identifiers(List<BaseIdentifierDt> theIdentifiers) {
return new TokenCriterion(getParamName(), theIdentifiers);
}
@Override
public ICriterion<TokenClientParam> identifiers(BaseIdentifierDt... theIdentifiers) {
return new TokenCriterion(getParamName(), Arrays.asList(theIdentifiers));
}
};
}
@Override
public String getParamName() {
return myParamName;
}
/**
* Create a search criterion that matches against the given system
* value but does not specify a code. This means that any code/identifier with
* the given system should match.
* <p>
* Use {@link #exactly()} if you want to specify a code.
* </p>
*/
public ICriterion<TokenClientParam> hasSystemWithAnyCode(String theSystem) {
return new TokenCriterion(getParamName(), theSystem, null);
}
public interface IMatches {
/**
* Creates a search criterion that matches against the given code, with no code system specified
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> code(String theIdentifier);
/**
* Creates a search criterion that matches against the given identifier (system and code if both are present, or whatever is present)
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifier(BaseIdentifierDt theIdentifier);
/**
* Creates a search criterion that matches against the given identifier, with no system specified
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifier(String theIdentifier);
/**
* Creates a search criterion that matches against the given collection of identifiers (system and code if both are present, or whatever is present).
* In the query URL that is generated, identifiers will be joined with a ',' to create an OR query.
*
* @param theIdentifiers
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifiers(BaseIdentifierDt... theIdentifiers);
/**
* Creates a search criterion that matches against the given collection of identifiers (system and code if both are present, or whatever is present).
* In the query URL that is generated, identifiers will be joined with a ',' to create an OR query.
*
* @param theIdentifiers
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifiers(List<BaseIdentifierDt> theIdentifiers);
/**
* Creates a search criterion that matches against the given code system and code
*
@ -106,53 +165,6 @@ public class TokenClientParam extends BaseClientParam implements IParam {
*/
ICriterion<TokenClientParam> systemAndIdentifier(String theSystem, String theIdentifier);
/**
* Creates a search criterion that matches against the given identifier, with no system specified
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifier(String theIdentifier);
/**
* Creates a search criterion that matches against the given code, with no code system specified
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> code(String theIdentifier);
/**
* Creates a search criterion that matches against the given identifier (system and code if both are present, or whatever is present)
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifier(BaseIdentifierDt theIdentifier);
/**
* Creates a search criterion that matches against the given collection of identifiers (system and code if both are present, or whatever is present).
* In the query URL that is generated, identifiers will be joined with a ',' to create an OR query.
*
* @param theIdentifiers
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifiers(List<BaseIdentifierDt> theIdentifiers);
/**
* Creates a search criterion that matches against the given collection of identifiers (system and code if both are present, or whatever is present).
* In the query URL that is generated, identifiers will be joined with a ',' to create an OR query.
*
* @param theIdentifiers
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifiers(BaseIdentifierDt... theIdentifiers);
}
}

View File

@ -1164,7 +1164,14 @@ public class SearchBuilder {
if (StringUtils.isNotBlank(code)) {
singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code));
} else {
singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
/*
* As of HAPI FHIR 1.5, if the client searched for a token with a system but
* no specified value this means to match all tokens with the given value.
*
* I'm not sure I agree with this, but hey.. FHIR-I voted and this was
* the result :)
*/
//singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
}
Predicate singleCode = theBuilder.and(toArray(singleCodePredicates));
return singleCode;

View File

@ -1373,6 +1373,38 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
}
}
@Test
public void testSearchTokenParamNoValue() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam1");
patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem").setDisplay("testSearchTokenParamDisplay");
myPatientDao.create(patient, mySrd);
patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
patient = new Patient();
patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null));
IBundleProvider retrieved = myPatientDao.search(map);
assertEquals(2, retrieved.size());
}
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", ""));
IBundleProvider retrieved = myPatientDao.search(map);
assertEquals(2, retrieved.size());
}
}
@Test
@Ignore
public void testSearchUnknownContentParam() {

View File

@ -88,6 +88,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Ignore;
import org.junit.Test;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.SummaryEnum;
@ -97,7 +98,9 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@ -116,6 +119,35 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
myDaoConfig.setAllowMultipleDelete(true);
}
@Test
public void testSearchTokenParamNoValue() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam1");
patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem").setDisplay("testSearchTokenParamDisplay");
myPatientDao.create(patient, mySrd);
patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
patient = new Patient();
patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
//@formatter:off
Bundle response = ourClient
.search()
.forResource(Patient.class)
.where(Patient.IDENTIFIER.hasSystemWithAnyCode("urn:system"))
.returnBundle(Bundle.class)
.execute();
//@formatter:on
assertEquals(2, response.getEntry().size());
}
@Test
public void testCreateConditional() {

View File

@ -731,17 +731,24 @@ public class GenericClientTest {
@Test
public void testSearchByDate() throws Exception {
String msg = getPatientFeedWithOneResult();
final String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
when(myHttpResponse.getEntity().getContent()).thenAnswer(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
@SuppressWarnings("deprecation")
Bundle response = client.search()
.forResource(Patient.class)
.encodedJson()
@ -755,7 +762,23 @@ public class GenericClientTest {
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?birthdate=%3C%3D2012-01-22&birthdate=%3E2011-01-01&_include=Patient.managingOrganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_sort=address&_count=123&_format=json", capt.getValue().getURI().toString());
assertEquals("http://example.com/fhir/Patient?birthdate=%3C%3D2012-01-22&birthdate=%3E2011-01-01&_include=Patient.managingOrganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_sort=address&_count=123&_format=json", capt.getAllValues().get(idx++).getURI().toString());
//@formatter:off
response = client.search()
.forResource(Patient.class)
.encodedJson()
.where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22"))
.and(Patient.BIRTHDATE.after().day("2011-01-01"))
.include(Patient.INCLUDE_MANAGINGORGANIZATION)
.sort().ascending(Patient.BIRTHDATE)
.sort().descending(Patient.NAME)
.sort().defaultOrder(Patient.ADDRESS)
.count(123)
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?birthdate=%3C%3D2012-01-22&birthdate=%3E2011-01-01&_include=Patient.managingOrganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_sort=address&_count=123&_format=json", capt.getAllValues().get(idx++).getURI().toString());
}
@ -1043,6 +1066,52 @@ public class GenericClientTest {
}
@SuppressWarnings("unused")
@Test
public void testSearchByTokenWithSystemAndNoCode() throws Exception {
final String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
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(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
//@formatter:off
Bundle response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.hasSystemWithAnyCode("urn:foo"))
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
//@formatter:on
//@formatter:off
response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", null))
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
//@formatter:on
//@formatter:off
response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", ""))
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
//@formatter:on
}
/**
* Test for #192
*/

View File

@ -305,6 +305,15 @@
sources. Thanks to Bill Denton for the pull
request!
</actiom>
<action type="fix">
JPA server now allows searching by token
parameter using a system only and no code,
giving a search for any tokens which match
the given token with any code. Previously the
expected behaviour for this search
was not clear in the spec and HAPI had different
behaviour from the other reference servers.
</action>
</release>
<release version="1.4" date="2016-02-04">
<action type="add">