diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java index 6fbb2d505f8..a545745c658 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java @@ -1,8 +1,9 @@ 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 @@ -34,9 +35,9 @@ import ch.qos.logback.core.db.dialect.MySQLDialect; */ public class Include { + private final boolean myImmutable; private boolean myRecurse; private String myValue; - private final boolean myImmutable; /** * Constructor for non-recursive include @@ -91,22 +92,6 @@ public class Include { 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} */ @@ -135,6 +120,73 @@ public class Include { 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() { return myRecurse; } @@ -151,20 +203,13 @@ public class Include { } /** - * Is this object {@link #toLocked() locked}? - */ - public boolean isLocked() { - return myImmutable; - } - - /** - * Return a new + * Return a new */ public Include toLocked() { Include retVal = new Include(myValue, myRecurse, true); return retVal; } - + @Override public String toString() { ToStringBuilder builder = new ToStringBuilder(this); @@ -172,36 +217,56 @@ public class Include { builder.append("recurse", myRecurse); return builder.toString(); } - + /** - * Creates and returns a new copy of this Include with the given type. The - * following table shows what will be returned: + * Creates and returns a new copy of this Include with the given type. The following table shows what will be + * returned: * - * - * - * - * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * *
Initial ContentstheResourceTypeOutput
Patient:careProviderOrganizationPatient:careProvider:Organization
Patient:careProvider:PractitionerOrganizationPatient:careProvider:Organization
Patient(any){@link IllegalStateException}
Initial ContentstheResourceTypeOutput
Patient:careProvider + * OrganizationPatient:careProvider:Organization
Patient:careProvider:Practitioner + * OrganizationPatient:careProvider:Organization
Patient + * (any){@link IllegalStateException}
* - * @param theResourceType 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 + * @param theResourceType + * 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) { StringBuilder b = new StringBuilder(); - b.append(myValue); - int firstColon = myValue.indexOf(':'); - if (firstColon == -1 || firstColon == b.length() - 1) { + + String paramType = getParamType(); + String paramName = getParamName(); + if (isBlank(paramType) || isBlank(paramName)) { throw new IllegalStateException("This include does not contain a value in the format [ResourceType]:[paramName]"); } - int secondColon = myValue.indexOf(':', firstColon + 1); - if (secondColon != -1) { - b.delete(secondColon, b.length()); - } - + b.append(paramType); b.append(":"); - b.append(theResourceType); + b.append(paramName); + + if (isNotBlank(theResourceType)) { + b.append(':'); + b.append(theResourceType); + } Include retVal = new Include(b.toString(), myRecurse, myImmutable); return retVal; } - + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 83b375b1507..a49496397e3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.jpa.dao; +import static org.apache.commons.lang3.StringUtils.defaultString; + /* * #%L * HAPI FHIR JPA Server @@ -1378,19 +1380,18 @@ public class SearchBuilder { if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) { paths = Collections.singletonList(nextInclude.getValue()); } else { - int colonIdx = nextInclude.getValue().indexOf(':'); - if (colonIdx < 2) { + String resType = nextInclude.getParamType(); + if (isBlank(resType)) { continue; } - String resType = nextInclude.getValue().substring(0, colonIdx); RuntimeResourceDefinition def = myContext.getResourceDefinition(resType); if (def == null) { ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); continue; } - String paramName = nextInclude.getValue().substring(colonIdx + 1); - RuntimeSearchParam param = def.getSearchParam(paramName); + String paramName = nextInclude.getParamName(); + RuntimeSearchParam param = isNotBlank(paramName) ? def.getSearchParam(paramName) : null; if (param == null) { ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); continue; @@ -1399,11 +1400,20 @@ public class SearchBuilder { paths = param.getPathsSplit(); } + String targetResourceType = defaultString(nextInclude.getParamTargetType(), null); 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 q = myEntityManager.createQuery(sql, ResourceLink.class); q.setParameter("src_path", nextPath); q.setParameter("target_pids", nextRoundMatches); + if (targetResourceType != null) { + q.setParameter("target_resource_type", targetResourceType); + } List results = q.getResultList(); for (ResourceLink resourceLink : results) { if (theReverseMode) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java index 8d45916f071..02b1db83eac 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java @@ -36,6 +36,7 @@ import javax.persistence.Table; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.hibernate.annotations.ColumnDefault; import org.hibernate.search.annotations.Field; @Entity @@ -64,6 +65,11 @@ public class ResourceLink implements Serializable { @Column(name = "SRC_RESOURCE_ID", insertable = false, updatable = false, nullable = false) 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) @JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = false) private ResourceTable myTargetResource; @@ -71,18 +77,21 @@ public class ResourceLink implements Serializable { @Column(name = "TARGET_RESOURCE_ID", insertable = false, updatable = false, nullable = false) @Field() 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() { super(); } public ResourceLink(String theSourcePath, ResourceTable theSourceResource, ResourceTable theTargetResource) { super(); - mySourcePath = theSourcePath; - mySourceResource = theSourceResource; - mySourceResourcePid = theSourceResource.getId(); - myTargetResource = theTargetResource; - myTargetResourcePid = theTargetResource.getId(); + setSourcePath(theSourcePath); + setSourceResource(theSourceResource); + setTargetResource(theTargetResource); } @Override @@ -140,12 +149,14 @@ public class ResourceLink implements Serializable { public void setSourceResource(ResourceTable theSourceResource) { mySourceResource = theSourceResource; mySourceResourcePid = theSourceResource.getId(); + mySourceResourceType = theSourceResource.getResourceType(); } public void setTargetResource(ResourceTable theTargetResource) { Validate.notNull(theTargetResource); myTargetResource = theTargetResource; myTargetResourcePid = theTargetResource.getId(); + myTargetResourceType = theTargetResource.getResourceType(); } @Override diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java index e1010cd45eb..03820122be1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java @@ -1462,6 +1462,13 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { patientId2 = myPatientDao.create(patient).getId().toUnqualifiedVersionless(); } + { + // Typed include + SearchParameterMap params = new SearchParameterMap(); + params.addInclude(Patient.INCLUDE_CAREPROVIDER.withType("Practitioner")); + List ids = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(ids, containsInAnyOrder(patientId, patientId2, practId2)); + } { // No includes SearchParameterMap params = new SearchParameterMap(); @@ -1545,13 +1552,6 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { List ids = toUnqualifiedVersionlessIds(myPatientDao.search(params)); assertThat(ids, containsInAnyOrder(orgId, patientId, patientId2, practId2)); } - { -// // Typed include -// SearchParameterMap params = new SearchParameterMap(); -// params.addInclude(Patient.INCLUDE_CAREPROVIDER.withType("Practitioner")); -// List ids = toUnqualifiedVersionlessIds(myPatientDao.search(params)); -// assertThat(ids, containsInAnyOrder(patientId, patientId2, practId2)); - } } @SuppressWarnings("unused") diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java index 8bc9e169ed6..ccbe4e00a69 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java @@ -177,6 +177,8 @@ public class IncludeTest { assertEquals("Patient:careProvider:Practitioner", new Include("Patient:careProvider", true).withType("Practitioner").getValue()); assertEquals(true, new Include("Patient:careProvider", true).withType("Practitioner").isRecurse()); 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(true, new Include("Patient:careProvider:Organization", true).toLocked().withType("Practitioner").isLocked()); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index a467f8728ec..b6e46d48b9a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -71,6 +71,11 @@ valueString instead of valueMarkdown. After discussion with Grahame, this appears to be incorrect behaviour so it has been fixed. + + Support target parameter type in _include / _revinclude values, e.g. + _include=Patient:careProvider:Organization. Thanks to Joe Portner + for reporting! +