Fix #240 - Support target resource type in server _include and _revinclude values

This commit is contained in:
jamesagnew 2015-12-14 21:50:35 -05:00
parent d95bd269c0
commit a623003a56
6 changed files with 159 additions and 66 deletions

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.model.api; package ca.uhn.fhir.model.api;
import org.apache.commons.lang3.builder.ToStringBuilder; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import ch.qos.logback.core.db.dialect.MySQLDialect; import org.apache.commons.lang3.builder.ToStringBuilder;
/* /*
* #%L * #%L
@ -34,9 +35,9 @@ import ch.qos.logback.core.db.dialect.MySQLDialect;
*/ */
public class Include { public class Include {
private final boolean myImmutable;
private boolean myRecurse; private boolean myRecurse;
private String myValue; private String myValue;
private final boolean myImmutable;
/** /**
* Constructor for <b>non-recursive</b> include * Constructor for <b>non-recursive</b> include
@ -91,22 +92,6 @@ public class Include {
return new Include(myValue, true); return new Include(myValue, true);
} }
public String getValue() {
return myValue;
}
/**
* See the note on equality on the {@link Include class documentation}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (myRecurse ? 1231 : 1237);
result = prime * result + ((myValue == null) ? 0 : myValue.hashCode());
return result;
}
/** /**
* See the note on equality on the {@link Include class documentation} * See the note on equality on the {@link Include class documentation}
*/ */
@ -135,6 +120,73 @@ public class Include {
return true; return true;
} }
/**
* Returns the portion of the value before the first colon
*/
public String getParamType() {
int firstColon = myValue.indexOf(':');
if (firstColon == -1 || firstColon == myValue.length() - 1) {
return null;
}
return myValue.substring(0, firstColon);
}
/**
* Returns the portion of the value after the first colon but before the second colon
*/
public String getParamName() {
int firstColon = myValue.indexOf(':');
if (firstColon == -1 || firstColon == myValue.length() - 1) {
return null;
}
int secondColon = myValue.indexOf(':', firstColon + 1);
if (secondColon != -1) {
return myValue.substring(firstColon + 1, secondColon);
} else {
return myValue.substring(firstColon + 1);
}
}
/**
* Returns the portion of the string after the second colon, or null if there are not two colons in the value.
*/
public String getParamTargetType() {
int firstColon = myValue.indexOf(':');
if (firstColon == -1 || firstColon == myValue.length() - 1) {
return null;
}
int secondColon = myValue.indexOf(':', firstColon + 1);
if (secondColon != -1) {
return myValue.substring(secondColon + 1);
} else {
return null;
}
}
public String getValue() {
return myValue;
}
/**
* See the note on equality on the {@link Include class documentation}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (myRecurse ? 1231 : 1237);
result = prime * result + ((myValue == null) ? 0 : myValue.hashCode());
return result;
}
/**
* Is this object {@link #toLocked() locked}?
*/
public boolean isLocked() {
return myImmutable;
}
public boolean isRecurse() { public boolean isRecurse() {
return myRecurse; return myRecurse;
} }
@ -150,13 +202,6 @@ public class Include {
myValue = theValue; myValue = theValue;
} }
/**
* Is this object {@link #toLocked() locked}?
*/
public boolean isLocked() {
return myImmutable;
}
/** /**
* Return a new * Return a new
*/ */
@ -174,32 +219,52 @@ public class Include {
} }
/** /**
* Creates and returns a new copy of this Include with the given type. The * Creates and returns a new copy of this Include with the given type. The following table shows what will be
* following table shows what will be returned: * returned:
* <table> * <table>
* <tr><th>Initial Contents</th><th>theResourceType</th><th>Output</th></tr> * <tr>
* <tr><td>Patient:careProvider</th><th>Organization</th><th>Patient:careProvider:Organization</th></tr> * <th>Initial Contents</th>
* <tr><td>Patient:careProvider:Practitioner</th><th>Organization</th><th>Patient:careProvider:Organization</th></tr> * <th>theResourceType</th>
* <tr><td>Patient</th><th>(any)</th><th>{@link IllegalStateException}</th></tr> * <th>Output</th>
* </tr>
* <tr>
* <td>Patient:careProvider</th>
* <th>Organization</th>
* <th>Patient:careProvider:Organization</th>
* </tr>
* <tr>
* <td>Patient:careProvider:Practitioner</th>
* <th>Organization</th>
* <th>Patient:careProvider:Organization</th>
* </tr>
* <tr>
* <td>Patient</th>
* <th>(any)</th>
* <th>{@link IllegalStateException}</th>
* </tr>
* </table> * </table>
* *
* @param theResourceType The resource type (e.g. "Organization") * @param theResourceType
* @return A new copy of the include. Note that if this include is {@link #toLocked() locked}, the returned include will be too * The resource type (e.g. "Organization")
* @return A new copy of the include. Note that if this include is {@link #toLocked() locked}, the returned include
* will be too
*/ */
public Include withType(String theResourceType) { public Include withType(String theResourceType) {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
b.append(myValue);
int firstColon = myValue.indexOf(':'); String paramType = getParamType();
if (firstColon == -1 || firstColon == b.length() - 1) { String paramName = getParamName();
if (isBlank(paramType) || isBlank(paramName)) {
throw new IllegalStateException("This include does not contain a value in the format [ResourceType]:[paramName]"); throw new IllegalStateException("This include does not contain a value in the format [ResourceType]:[paramName]");
} }
int secondColon = myValue.indexOf(':', firstColon + 1); b.append(paramType);
if (secondColon != -1) {
b.delete(secondColon, b.length());
}
b.append(":"); b.append(":");
b.append(paramName);
if (isNotBlank(theResourceType)) {
b.append(':');
b.append(theResourceType); b.append(theResourceType);
}
Include retVal = new Include(b.toString(), myRecurse, myImmutable); Include retVal = new Include(b.toString(), myRecurse, myImmutable);
return retVal; return retVal;
} }

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
import static org.apache.commons.lang3.StringUtils.defaultString;
/* /*
* #%L * #%L
* HAPI FHIR JPA Server * HAPI FHIR JPA Server
@ -1378,19 +1380,18 @@ public class SearchBuilder {
if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) { if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) {
paths = Collections.singletonList(nextInclude.getValue()); paths = Collections.singletonList(nextInclude.getValue());
} else { } else {
int colonIdx = nextInclude.getValue().indexOf(':'); String resType = nextInclude.getParamType();
if (colonIdx < 2) { if (isBlank(resType)) {
continue; continue;
} }
String resType = nextInclude.getValue().substring(0, colonIdx);
RuntimeResourceDefinition def = myContext.getResourceDefinition(resType); RuntimeResourceDefinition def = myContext.getResourceDefinition(resType);
if (def == null) { if (def == null) {
ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
continue; continue;
} }
String paramName = nextInclude.getValue().substring(colonIdx + 1); String paramName = nextInclude.getParamName();
RuntimeSearchParam param = def.getSearchParam(paramName); RuntimeSearchParam param = isNotBlank(paramName) ? def.getSearchParam(paramName) : null;
if (param == null) { if (param == null) {
ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
continue; continue;
@ -1399,11 +1400,20 @@ public class SearchBuilder {
paths = param.getPathsSplit(); paths = param.getPathsSplit();
} }
String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
for (String nextPath : paths) { for (String nextPath : paths) {
String sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; String sql;
if (targetResourceType != null) {
sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type";
} else {
sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)";
}
TypedQuery<ResourceLink> q = myEntityManager.createQuery(sql, ResourceLink.class); TypedQuery<ResourceLink> q = myEntityManager.createQuery(sql, ResourceLink.class);
q.setParameter("src_path", nextPath); q.setParameter("src_path", nextPath);
q.setParameter("target_pids", nextRoundMatches); q.setParameter("target_pids", nextRoundMatches);
if (targetResourceType != null) {
q.setParameter("target_resource_type", targetResourceType);
}
List<ResourceLink> results = q.getResultList(); List<ResourceLink> results = q.getResultList();
for (ResourceLink resourceLink : results) { for (ResourceLink resourceLink : results) {
if (theReverseMode) { if (theReverseMode) {

View File

@ -36,6 +36,7 @@ import javax.persistence.Table;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Field;
@Entity @Entity
@ -64,6 +65,11 @@ public class ResourceLink implements Serializable {
@Column(name = "SRC_RESOURCE_ID", insertable = false, updatable = false, nullable = false) @Column(name = "SRC_RESOURCE_ID", insertable = false, updatable = false, nullable = false)
private Long mySourceResourcePid; private Long mySourceResourcePid;
@Column(name = "SOURCE_RESOURCE_TYPE", nullable=false, length=ResourceTable.RESTYPE_LEN)
@ColumnDefault("''") // TODO: remove this (it's only here for simplifying upgrades of 1.3 -> 1.4)
@Field()
private String mySourceResourceType;
@ManyToOne(optional = false, fetch=FetchType.LAZY) @ManyToOne(optional = false, fetch=FetchType.LAZY)
@JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = false) @JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = false)
private ResourceTable myTargetResource; private ResourceTable myTargetResource;
@ -72,17 +78,20 @@ public class ResourceLink implements Serializable {
@Field() @Field()
private Long myTargetResourcePid; private Long myTargetResourcePid;
@Column(name = "TARGET_RESOURCE_TYPE", nullable=false, length=ResourceTable.RESTYPE_LEN)
@ColumnDefault("''") // TODO: remove this (it's only here for simplifying upgrades of 1.3 -> 1.4)
@Field()
private String myTargetResourceType;
public ResourceLink() { public ResourceLink() {
super(); super();
} }
public ResourceLink(String theSourcePath, ResourceTable theSourceResource, ResourceTable theTargetResource) { public ResourceLink(String theSourcePath, ResourceTable theSourceResource, ResourceTable theTargetResource) {
super(); super();
mySourcePath = theSourcePath; setSourcePath(theSourcePath);
mySourceResource = theSourceResource; setSourceResource(theSourceResource);
mySourceResourcePid = theSourceResource.getId(); setTargetResource(theTargetResource);
myTargetResource = theTargetResource;
myTargetResourcePid = theTargetResource.getId();
} }
@Override @Override
@ -140,12 +149,14 @@ public class ResourceLink implements Serializable {
public void setSourceResource(ResourceTable theSourceResource) { public void setSourceResource(ResourceTable theSourceResource) {
mySourceResource = theSourceResource; mySourceResource = theSourceResource;
mySourceResourcePid = theSourceResource.getId(); mySourceResourcePid = theSourceResource.getId();
mySourceResourceType = theSourceResource.getResourceType();
} }
public void setTargetResource(ResourceTable theTargetResource) { public void setTargetResource(ResourceTable theTargetResource) {
Validate.notNull(theTargetResource); Validate.notNull(theTargetResource);
myTargetResource = theTargetResource; myTargetResource = theTargetResource;
myTargetResourcePid = theTargetResource.getId(); myTargetResourcePid = theTargetResource.getId();
myTargetResourceType = theTargetResource.getResourceType();
} }
@Override @Override

View File

@ -1462,6 +1462,13 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
patientId2 = myPatientDao.create(patient).getId().toUnqualifiedVersionless(); patientId2 = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
} }
{
// Typed include
SearchParameterMap params = new SearchParameterMap();
params.addInclude(Patient.INCLUDE_CAREPROVIDER.withType("Practitioner"));
List<IIdType> ids = toUnqualifiedVersionlessIds(myPatientDao.search(params));
assertThat(ids, containsInAnyOrder(patientId, patientId2, practId2));
}
{ {
// No includes // No includes
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
@ -1545,13 +1552,6 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
List<IIdType> ids = toUnqualifiedVersionlessIds(myPatientDao.search(params)); List<IIdType> ids = toUnqualifiedVersionlessIds(myPatientDao.search(params));
assertThat(ids, containsInAnyOrder(orgId, patientId, patientId2, practId2)); assertThat(ids, containsInAnyOrder(orgId, patientId, patientId2, practId2));
} }
{
// // Typed include
// SearchParameterMap params = new SearchParameterMap();
// params.addInclude(Patient.INCLUDE_CAREPROVIDER.withType("Practitioner"));
// List<IIdType> ids = toUnqualifiedVersionlessIds(myPatientDao.search(params));
// assertThat(ids, containsInAnyOrder(patientId, patientId2, practId2));
}
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")

View File

@ -177,6 +177,8 @@ public class IncludeTest {
assertEquals("Patient:careProvider:Practitioner", new Include("Patient:careProvider", true).withType("Practitioner").getValue()); assertEquals("Patient:careProvider:Practitioner", new Include("Patient:careProvider", true).withType("Practitioner").getValue());
assertEquals(true, new Include("Patient:careProvider", true).withType("Practitioner").isRecurse()); assertEquals(true, new Include("Patient:careProvider", true).withType("Practitioner").isRecurse());
assertEquals(false, new Include("Patient:careProvider:Organization", true).withType("Practitioner").isLocked()); assertEquals(false, new Include("Patient:careProvider:Organization", true).withType("Practitioner").isLocked());
assertEquals("Practitioner", new Include("Patient:careProvider", true).withType("Practitioner").getParamTargetType());
assertEquals(null, new Include("Patient:careProvider", true).getParamTargetType());
assertEquals("Patient:careProvider:Practitioner", new Include("Patient:careProvider:Organization", true).withType("Practitioner").getValue()); assertEquals("Patient:careProvider:Practitioner", new Include("Patient:careProvider:Organization", true).withType("Practitioner").getValue());
assertEquals(true, new Include("Patient:careProvider:Organization", true).toLocked().withType("Practitioner").isLocked()); assertEquals(true, new Include("Patient:careProvider:Organization", true).toLocked().withType("Practitioner").isLocked());

View File

@ -71,6 +71,11 @@
valueString instead of valueMarkdown. After discussion with Grahame, this appears to valueString instead of valueMarkdown. After discussion with Grahame, this appears to
be incorrect behaviour so it has been fixed. be incorrect behaviour so it has been fixed.
</action> </action>
<action type="add" issue="240">
Support target parameter type in _include / _revinclude values, e.g.
_include=Patient:careProvider:Organization. Thanks to Joe Portner
for reporting!
<action>
</release> </release>
<release version="1.3" date="2015-11-14"> <release version="1.3" date="2015-11-14">
<action type="add"> <action type="add">