Implements support for _tag in JPA server and client

This commit is contained in:
James Agnew 2015-07-30 07:47:37 -04:00
parent 7988cc3993
commit ab2129d651
13 changed files with 245 additions and 32 deletions

View File

@ -269,6 +269,7 @@ public class GenericClientExample {
.encodedJson()
.where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22"))
.and(Patient.BIRTHDATE.after().day("2011-01-01"))
.withTag("http://acme.org/codes", "needs-review")
.include(Patient.INCLUDE_ORGANIZATION)
.revInclude(Provenance.INCLUDE_TARGET)
.lastUpdated(new DateRangeParam("2011-01-01", null))

View File

@ -129,6 +129,7 @@ import ca.uhn.fhir.rest.method.ValidateMethodBindingDstu1;
import ca.uhn.fhir.rest.method.ValidateMethodBindingDstu2;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory;
@ -1556,6 +1557,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
myCriterion.populateParamList(params);
for (TokenParam next : myTags) {
addParam(params, Constants.PARAM_TAG, next.getValueAsQueryToken());
}
for (Include next : myInclude) {
addParam(params, Constants.PARAM_INCLUDE, next.getValue());
}
@ -1689,6 +1694,15 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
private List<TokenParam> myTags = new ArrayList<TokenParam>();
@Override
public IQuery<Object> withTag(String theSystem, String theCode) {
Validate.notBlank(theCode, "theCode must not be null or empty");
myTags.add(new TokenParam(theSystem, theCode));
return this;
}
}
@SuppressWarnings("rawtypes")

View File

@ -251,6 +251,12 @@ public interface IGenericClient extends IRestfulClient {
@Override
void registerInterceptor(IClientInterceptor theInterceptor);
/**
* Search for resources matching a given set of criteria. Searching is a very powerful
* feature in FHIR with many features for specifying exactly what should be seaerched for
* and how it should be returned. See the <a href="http://www.hl7.org/fhir/search.html">specification on search</a>
* for more information.
*/
IUntypedQuery search();
/**

View File

@ -37,6 +37,14 @@ public interface IQuery<T> extends IClientExecutable<IQuery<T>, T>, IBaseQuery<I
IQuery<T> limitTo(int theLimitTo);
/**
* Match only resources where the resource has the given tag. This parameter corresponds to
* the <code>_tag</code> URL parameter.
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<T> withTag(String theSystem, String theCode);
/**
* Forces the query to perform the search using the given method (allowable methods are described in the
* <a href="http://www.hl7.org/implement/standards/fhir/http.html#search">FHIR Specification Section 2.1.11</a>)

View File

@ -110,6 +110,7 @@ public class Constants {
public static final String PARAM_SORT_ASC = "_sort:asc";
public static final String PARAM_SORT_DESC = "_sort:desc";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TAG = "_tag";
public static final String PARAM_VALIDATE = "_validate";
public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";

View File

@ -59,7 +59,6 @@ import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.omg.PortableInterceptor.InterceptorOperations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.PlatformTransactionManager;
@ -86,6 +85,7 @@ import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.entity.ResourceLink;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.util.StopWatch;
@ -293,6 +293,71 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return new HashSet<Long>(q.getResultList());
}
private Set<Long> addPredicateTag(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList) {
Set<Long> pids = thePids;
if (theList == null || theList.isEmpty()) {
return pids;
}
for (List<? extends IQueryParameterType> nextAndParams : theList) {
boolean haveTags = false;
for (IQueryParameterType nextParamUncasted : nextAndParams) {
TokenParam nextParam = (TokenParam) nextParamUncasted;
if (isNotBlank(nextParam.getValue())) {
haveTags = true;
} else if (isNotBlank(nextParam.getSystem())) {
throw new InvalidRequestException("Invalid _tag parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken());
}
}
if (!haveTags) {
continue;
}
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
Root<ResourceTag> from = cq.from(ResourceTag.class);
cq.select(from.get("myResourceId").as(Long.class));
List<Predicate> andPredicates = new ArrayList<Predicate>();
andPredicates.add(builder.equal(from.get("myResourceType"), myResourceName));
List<Predicate> orPredicates = new ArrayList<Predicate>();
for (IQueryParameterType nextOrParams : nextAndParams) {
TokenParam nextParam = (TokenParam) nextOrParams;
From<ResourceTag, TagDefinition> defJoin = from.join("myTag");
Predicate codePrediate = builder.equal(defJoin.get("myCode"), nextParam.getValue());
if (isBlank(nextParam.getValue())) {
continue;
}
if (isNotBlank(nextParam.getSystem())) {
Predicate systemPrediate = builder.equal(defJoin.get("mySystem"), nextParam.getSystem());
orPredicates.add(builder.and(systemPrediate, codePrediate));
} else {
orPredicates.add(codePrediate);
}
}
if (orPredicates.isEmpty() == false) {
andPredicates.add(builder.or(orPredicates.toArray(new Predicate[0])));
}
Predicate masterCodePredicate = builder.and(andPredicates.toArray(new Predicate[0]));
if (pids.size() > 0) {
Predicate inPids = (from.get("myResourceId").in(pids));
cq.where(builder.and(masterCodePredicate, inPids));
} else {
cq.where(masterCodePredicate);
}
TypedQuery<Long> q = myEntityManager.createQuery(cq);
pids = new HashSet<Long>(q.getResultList());
}
return pids;
}
private boolean addPredicateMissingFalseIfPresent(CriteriaBuilder theBuilder, String theParamName, Root<? extends BaseResourceIndexedSearchParam> from, List<Predicate> codePredicates,
IQueryParameterType nextOr) {
boolean missingFalse = false;
@ -635,11 +700,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
RuntimeResourceDefinition resDef = getContext().getResourceDefinition(ref.getResourceType());
resourceTypes.add(resDef.getImplementingClass());
}
boolean foundChainMatch = false;
for (Class<? extends IBaseResource> nextType : resourceTypes) {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(nextType);
String chain = ref.getChain();
String remainingChain = null;
int chainDotIndex = chain.indexOf('.');
@ -658,23 +723,23 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName(), param);
continue;
}
IQueryParameterType chainValue;
if (remainingChain != null) {
if (param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", new Object[] { nextType.getSimpleName(), chain, remainingChain });
continue;
}
chainValue = new ReferenceParam();
chainValue.setValueAsQueryToken(null, resourceId);
((ReferenceParam)chainValue).setChain(remainingChain);
((ReferenceParam) chainValue).setChain(remainingChain);
} else {
chainValue = toParameterType(param, resourceId);
}
foundChainMatch = true;
Set<Long> pids = dao.searchForIds(chain, chainValue);
if (pids.isEmpty()) {
continue;
@ -684,7 +749,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
codePredicates.add(eq);
}
if (!foundChainMatch) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain()));
}
@ -894,7 +959,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
protected IBaseOperationOutcome createErrorOperationOutcome(String theMessage) {
return createOperationOutcome(IssueSeverityEnum.ERROR.getCode(), theMessage);
}
protected IBaseOperationOutcome createInfoOperationOutcome(String theMessage) {
return createOperationOutcome(IssueSeverityEnum.INFORMATION.getCode(), theMessage);
}
@ -1017,12 +1082,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
}
if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
throw new InvalidRequestException("Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH
+ "): " + system);
throw new InvalidRequestException(
"Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
}
if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
throw new InvalidRequestException("Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH
+ "): " + code);
throw new InvalidRequestException(
"Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
}
ArrayList<Predicate> singleCodePredicates = (new ArrayList<Predicate>());
@ -1096,13 +1161,13 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
}
From<?, ?> stringJoin = theFrom.join(joinAttrName, JoinType.INNER);
if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
thePredicates.add(stringJoin.get("mySourcePath").as(String.class).in(param.getPathsSplit()));
} else {
thePredicates.add(theBuilder.equal(stringJoin.get("myParamName"), theSort.getParamName()));
}
// Predicate p = theBuilder.equal(stringJoin.get("myParamName"), theSort.getParamName());
// Predicate pn = theBuilder.isNull(stringJoin.get("myParamName"));
// thePredicates.add(theBuilder.or(p, pn));
@ -1197,7 +1262,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
notifyWriteCompleted();
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
@ -1594,8 +1659,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
if (entity == null) {
if (theId.hasVersionIdPart()) {
TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery(
"SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
TypedQuery<ResourceHistoryTable> q = myEntityManager
.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
q.setParameter("RID", pid);
q.setParameter("RTYP", myResourceName);
q.setParameter("RVER", Long.parseLong(theId.getVersionIdPart()));
@ -1930,6 +1995,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
pids = addPredicateLanguage(pids, nextParamEntry.getValue());
} else if (nextParamName.equals("_tag")) {
pids = addPredicateTag(pids, nextParamEntry.getValue());
} else {
RuntimeSearchParam nextParamDef = resourceDef.getSearchParam(nextParamName);
@ -2136,7 +2205,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
ResourceTable savedEntity = updateEntity(theResource, entity, true, null, thePerformIndexing, true);
notifyWriteCompleted();
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
@ -2160,8 +2229,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
private void validateResourceType(BaseHasResource entity) {
if (!myResourceName.equals(entity.getResourceType())) {
throw new ResourceNotFoundException("Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type "
+ entity.getResourceType());
throw new ResourceNotFoundException(
"Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type " + entity.getResourceType());
}
}

View File

@ -44,12 +44,12 @@ public class ResourceTag extends BaseTag {
@JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID")
private ResourceTable myResource;
@Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN,nullable=false)
@Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false)
private String myResourceType;
@Column(name="RES_ID", insertable=false,updatable=false)
@Column(name = "RES_ID", insertable = false, updatable = false)
private Long myResourceId;
public Long getResourceId() {
return myResourceId;
}

View File

@ -39,9 +39,7 @@ import ca.uhn.fhir.model.api.Tag;
//@formatter:on
@Entity
@Table(name = "HFJ_TAG_DEF", uniqueConstraints = {
@UniqueConstraint(columnNames = { "TAG_TYPE", "TAG_SYSTEM", "TAG_CODE" })
})
@Table(name = "HFJ_TAG_DEF", uniqueConstraints = { @UniqueConstraint(columnNames = { "TAG_TYPE", "TAG_SYSTEM", "TAG_CODE" }) })
//@formatter:off
public class TagDefinition implements Serializable {
@ -89,6 +87,10 @@ public class TagDefinition implements Serializable {
return myDisplay;
}
public Long getId() {
return myId;
}
public String getSystem() {
return mySystem;
}

View File

@ -87,6 +87,7 @@ import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IBundleProvider;
@ -1873,6 +1874,80 @@ public class FhirResourceDaoDstu2Test extends BaseJpaTest {
}
@Test
public void testSearchWithTagParameter() {
String methodName = "testSearchWithTagParameter";
IIdType tag1id;
{
Organization org = new Organization();
org.getNameElement().setValue("FOO");
TagList tagList = new TagList();
tagList.addTag("urn:taglist", methodName + "1a");
tagList.addTag("urn:taglist", methodName + "1b");
ResourceMetadataKeyEnum.TAG_LIST.put(org, tagList);
tag1id = ourOrganizationDao.create(org).getId().toUnqualifiedVersionless();
}
IIdType tag2id;
{
Organization org = new Organization();
org.getNameElement().setValue("FOO");
TagList tagList = new TagList();
tagList.addTag("urn:taglist", methodName + "2a");
tagList.addTag("urn:taglist", methodName + "2b");
ResourceMetadataKeyEnum.TAG_LIST.put(org, tagList);
tag2id = ourOrganizationDao.create(org).getId().toUnqualifiedVersionless();
}
{
// One tag
SearchParameterMap params = new SearchParameterMap();
params.add("_tag", new TokenParam("urn:taglist", methodName + "1a"));
List<IIdType> patients = toUnqualifiedVersionlessIds(ourOrganizationDao.search(params));
assertThat(patients, containsInAnyOrder(tag1id));
}
{
// Code only
SearchParameterMap params = new SearchParameterMap();
params.add("_tag", new TokenParam(null, methodName + "1a"));
List<IIdType> patients = toUnqualifiedVersionlessIds(ourOrganizationDao.search(params));
assertThat(patients, containsInAnyOrder(tag1id));
}
{
// Or tags
SearchParameterMap params = new SearchParameterMap();
TokenOrListParam orListParam = new TokenOrListParam();
orListParam.add(new TokenParam("urn:taglist", methodName + "1a"));
orListParam.add(new TokenParam("urn:taglist", methodName + "2a"));
params.add("_tag", orListParam);
List<IIdType> patients = toUnqualifiedVersionlessIds(ourOrganizationDao.search(params));
assertThat(patients, containsInAnyOrder(tag1id, tag2id));
}
// TODO: get multiple/AND working
{
// And tags
SearchParameterMap params = new SearchParameterMap();
TokenAndListParam andListParam = new TokenAndListParam();
andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "1a"));
andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "2a"));
params.add("_tag", andListParam);
List<IIdType> patients = toUnqualifiedVersionlessIds(ourOrganizationDao.search(params));
assertEquals(0, patients.size());
}
{
// And tags
SearchParameterMap params = new SearchParameterMap();
TokenAndListParam andListParam = new TokenAndListParam();
andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "1a"));
andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "1b"));
params.add("_tag", andListParam);
List<IIdType> patients = toUnqualifiedVersionlessIds(ourOrganizationDao.search(params));
assertThat(patients, containsInAnyOrder(tag1id));
}
}
@Test
public void testSearchWithIncludes() {
IIdType parentOrgId;

View File

@ -693,6 +693,34 @@ public class GenericClientTest {
}
@SuppressWarnings("unused")
@Test
public void testSearchByTag() throws Exception {
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")));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
//@formatter:off
Bundle response = client.search()
.forResource(Patient.class)
.withTag("urn:foo", "123")
.withTag("urn:bar", "456")
.execute();
//@formatter:on
assertEquals(
"http://example.com/fhir/Patient?_tag=urn%3Afoo%7C123&_tag=urn%3Abar%7C456",
capt.getValue().getURI().toString());
}
@SuppressWarnings("unused")
@Test
public void testSearchWithReverseInclude() throws Exception {

View File

@ -49,6 +49,11 @@ public class ${className}ResourceProvider extends
@Description(shortDefinition="The resource language")
@OptionalParam(name="_language")
StringParam theResourceLanguage,
@Description(shortDefinition="Search for resources which have the given tag")
@OptionalParam(name="_tag")
TokenAndListParam theSearchForTag,
#foreach ( $param in $searchParams ) #{if}(true) #{end}
@Description(shortDefinition="${param.description}")
@ -113,10 +118,11 @@ public class ${className}ResourceProvider extends
startRequest(theServletRequest);
try {
SearchParameterMap paramMap = new SearchParameterMap();
paramMap.add("_id", theId);
paramMap.add("_language", theResourceLanguage);
paramMap.add("_id", theId);
paramMap.add("_language", theResourceLanguage);
paramMap.add("_tag", theSearchForTag);
#foreach ( $param in $searchParams )
paramMap.add("${param.name}", the${param.nameCapitalized});
paramMap.add("${param.name}", the${param.nameCapitalized});
#end
#if ( $version != 'dstu' )
paramMap.setRevIncludes(theRevIncludes);

View File

@ -64,6 +64,9 @@
package lists are up to date. Thanks to GitHub user
Brian S. Corbin (@corbinbs) for thr contribution!
</action>
<action type="add">
JPA server and generic client now both support the _tag search parameter
</action>
</release>
<release version="1.1" date="2015-07-13">
<action type="add">

View File

@ -178,7 +178,7 @@
<h4>Search - Other Query Options</h4>
<p>
The fluent search also has methods for sorting, limiting, specifying
JSON encoding, _include, _revinclude, _lastUpdated, etc.
JSON encoding, _include, _revinclude, _lastUpdated, _tag, etc.
</p>
<macro name="snippet">
<param name="id" value="searchAdv" />