Open bill-bishop-optum opened 3 years ago
Hi.
So I am seeing the same issue here.k
SHORT VERSION:
WithOUT any "rev-includes" being "coded up" to @Search Resource Providers, ... "searchRevInclude" entries are showing up. And "criss crossing" the FHIR-resources.
But I think there is some "criss crossing" going on.
I have a very simple demo FHIR server.
So if I add (only) a "Coverage" Resource provider:
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.r4.model.Coverage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CoverageResourceProvider implements IResourceProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(CoverageResourceProvider.class);
public CoverageResourceProvider() {
}
@Override
public Class<Coverage> getResourceType() {
return Coverage.class;
}
@Search
public IBundleProvider search(
@OptionalParam(name = Coverage.SP_PATIENT) TokenParam patient) { // No reverse includes
return new SimpleBundleProvider(5);
}
}
I get a capability statement like below (and it looks correct).
{
"resourceType": "CapabilityStatement",
"id": "89bcadf6-b645-4a32-a81a-2a80e0ce01af",
"name": "RestServer",
"status": "active",
"date": "2023-06-26T13:46:33.722-04:00",
"publisher": "Not provided",
"kind": "instance",
"software": {
"name": "quick-sample-fhir-server-not-for-production",
"version": "1.0.0"
},
"implementation": {
"description": "quick-sample-fhir-server-not-for-production implementation",
"url": "/myr4fhirserver/*"
},
"fhirVersion": "4.0.1",
"format": [
"application/fhir+xml",
"xml",
"application/fhir+json",
"json"
],
"rest": [
{
"mode": "server",
"resource": [
{
"type": "Coverage",
"profile": "http://hl7.org/fhir/StructureDefinition/Coverage",
"interaction": [
{
"code": "search-type"
}
],
"searchInclude": [
"*"
],
"searchParam": [
{
"name": "patient",
"type": "token",
"documentation": "Retrieve coverages for a patient"
}
]
},
{
"type": "OperationDefinition",
"profile": "http://hl7.org/fhir/StructureDefinition/OperationDefinition",
"interaction": [
{
"code": "read"
}
],
"searchInclude": [
"*"
]
}
]
}
]
}
Note, there are NO "searchRevInclude" entries in the above.
Ok, now I add a SECOND ResourceProvider.
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.r4.model.MedicationDispense;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class MedicationDispenseResourceProvider implements IResourceProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(MedicationDispenseResourceProvider.class);
public MedicationDispenseResourceProvider() {
}
@Override
public Class<MedicationDispense> getResourceType() {
return MedicationDispense.class;
}
@Search
public IBundleProvider search(
@OptionalParam(name = MedicationDispense.SP_PATIENT) ReferenceParam patientRef) { // No reverse includes
return new SimpleBundleProvider(7);
}
}
Note, I keep the first Resource Provider. So I have Coverage and MedicationDispense "wired up".
And here is my capability statement:
{
"resourceType": "CapabilityStatement",
"id": "043a2eeb-5956-4a65-b292-f99108af66c4",
"name": "RestServer",
"status": "active",
"date": "2023-06-26T14:22:43.667-04:00",
"publisher": "Not provided",
"kind": "instance",
"software": {
"name": "quick-sample-fhir-server-not-for-production",
"version": "1.0.0"
},
"implementation": {
"description": "quick-sample-fhir-server-not-for-production implementation",
"url": "/myr4fhirserver/*"
},
"fhirVersion": "4.0.1",
"format": [
"application/fhir+xml",
"xml",
"application/fhir+json",
"json"
],
"rest": [
{
"mode": "server",
"resource": [
{
"type": "Coverage",
"profile": "http://hl7.org/fhir/StructureDefinition/Coverage",
"interaction": [
{
"code": "search-type"
}
],
"searchInclude": [
"*"
],
"searchRevInclude": [
"MedicationDispense:patient"
],
"searchParam": [
{
"name": "patient",
"type": "token",
"documentation": "Retrieve coverages for a patient"
}
]
},
{
"type": "MedicationDispense",
"profile": "http://hl7.org/fhir/StructureDefinition/MedicationDispense",
"interaction": [
{
"code": "search-type"
}
],
"searchInclude": [
"*",
"MedicationDispense:patient"
],
"searchRevInclude": [
"MedicationDispense:patient"
],
"searchParam": [
{
"name": "patient",
"type": "reference",
"documentation": "The identity of a patient to list dispenses for"
}
]
},
{
"type": "OperationDefinition",
"profile": "http://hl7.org/fhir/StructureDefinition/OperationDefinition",
"interaction": [
{
"code": "read"
}
],
"searchInclude": [
"*"
],
"searchRevInclude": [
"MedicationDispense:patient"
]
}
]
}
]
}
Note, that under "Coverage" I have
"searchRevInclude": [
"MedicationDispense:patient"
],
and under MedicationDispense I have:
"searchInclude": [
"*",
"MedicationDispense:patient"
],
"searchRevInclude": [
"MedicationDispense:patient"
],
which does not make any sense to me.
I have "gotten into the Hapi code" here:
package ca.uhn.fhir.rest.server.provider;
public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> {
and
// Add RevInclude to CapabilityStatement.rest.resource
if (myRestResourceRevIncludesEnabled) {
NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName);
if (resourceRevIncludes.isEmpty()) {
TreeSet<String> revIncludes = new TreeSet<>();
for (String nextResourceName : resourceToMethods.keySet()) {
if (isBlank(nextResourceName)) {
continue;
}
for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) {
if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
if (isNotBlank(t.getName())) {
boolean appropriateTarget = false;
if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) {
appropriateTarget = true;
}
if (appropriateTarget) {
revIncludes.add(nextResourceName + ":" + t.getName());
}
}
}
}
}
for (String nextInclude : revIncludes) {
terser.addElement(resource, "searchRevInclude", nextInclude);
}
} else {
for (String resourceInclude : resourceRevIncludes) {
terser.addElement(resource, "searchRevInclude", resourceInclude);
}
}
}
weird part #1 to me is:
for (String nextResourceName : resourceToMethods.keySet()) {
so while in the "current FHIR resource" loop, it looks at resourceToMethods. But I readily admit, I may not understand the intention there.
But more to the point (in the bug report)
and this looks "suspect" to me.
if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) {
appropriateTarget = true;
}
especially the second || clause "t.getTargets().isEmpty".
But while I am in the "Coverage" "loop" of the above code......it is looking through all the (defined) FHIR resources. .. and that feels weird to me.
But that is definitely where Coverage is (errantly) getting a rev-include of ""MedicationDispense:patient"" added to the capability statement.
I am on :
hapiFhirVersion = '6.4.4'
Helpful links:
So, yeah, if you add 3 ResourceProviders, you can really see the "Criss Cross" that is occurring.
I also upgrade to Hapi FHIR Server 6.6.1.
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.r4.model.Coverage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CoverageResourceProvider implements IResourceProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(CoverageResourceProvider.class);
public CoverageResourceProvider() {
}
@Override
public Class<Coverage> getResourceType() {
return Coverage.class;
}
@Search
public IBundleProvider search(
@OptionalParam(name = Coverage.SP_PATIENT) ReferenceParam patient) { // No reverse includes
return new SimpleBundleProvider(5);
}
}
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.r4.model.MedicationDispense;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class MedicationDispenseResourceProvider implements IResourceProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(MedicationDispenseResourceProvider.class);
public MedicationDispenseResourceProvider() {
}
@Override
public Class<MedicationDispense> getResourceType() {
return MedicationDispense.class;
}
@Search
public IBundleProvider search(
@OptionalParam(name = MedicationDispense.SP_PATIENT) ReferenceParam patientRef) { // No reverse includes
return new SimpleBundleProvider(7);
}
}
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.r4.model.Observation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ObservationResourceProvider implements IResourceProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(ObservationResourceProvider.class);
public ObservationResourceProvider() {
}
@Override
public Class<Observation> getResourceType() {
return Observation.class;
}
@Search
public IBundleProvider search(
@OptionalParam(name = Observation.SP_PATIENT) ReferenceParam patient) { // No reverse includes
return new SimpleBundleProvider(9);
}
}
and
Capability Statement:
{
"resourceType": "CapabilityStatement",
"id": "79cd9005-3e19-47d0-bb2d-cd9ab8a507d0",
"name": "RestServer",
"status": "active",
"date": "2023-06-28T10:12:58.314-04:00",
"publisher": "Not provided",
"kind": "instance",
"software": {
"name": "quick-sample-fhir-server-not-for-production",
"version": "1.0.0"
},
"implementation": {
"description": "quick-sample-fhir-server-not-for-production implementation",
"url": "/myr4fhirserver/*"
},
"fhirVersion": "4.0.1",
"format": [
"application/fhir+xml",
"xml",
"application/fhir+json",
"json"
],
"rest": [
{
"mode": "server",
"resource": [
{
"type": "Coverage",
"profile": "http://hl7.org/fhir/StructureDefinition/Coverage",
"interaction": [
{
"code": "search-type"
}
],
"searchInclude": [
"*",
"Coverage:patient"
],
"searchRevInclude": [
"Coverage:patient",
"MedicationDispense:patient",
"Observation:patient"
],
"searchParam": [
{
"name": "patient",
"type": "reference",
"documentation": "Retrieve coverages for a patient"
}
]
},
{
"type": "MedicationDispense",
"profile": "http://hl7.org/fhir/StructureDefinition/MedicationDispense",
"interaction": [
{
"code": "search-type"
}
],
"searchInclude": [
"*",
"MedicationDispense:patient"
],
"searchRevInclude": [
"Coverage:patient",
"MedicationDispense:patient",
"Observation:patient"
],
"searchParam": [
{
"name": "patient",
"type": "reference",
"documentation": "The identity of a patient to list dispenses for"
}
]
},
{
"type": "Observation",
"profile": "http://hl7.org/fhir/StructureDefinition/Observation",
"interaction": [
{
"code": "search-type"
}
],
"searchInclude": [
"*",
"Observation:patient"
],
"searchRevInclude": [
"Coverage:patient",
"MedicationDispense:patient",
"Observation:patient"
],
"searchParam": [
{
"name": "patient",
"type": "reference",
"documentation": "The subject that the observation is about (if patient)"
}
]
},
{
"type": "OperationDefinition",
"profile": "http://hl7.org/fhir/StructureDefinition/OperationDefinition",
"interaction": [
{
"code": "read"
}
],
"searchInclude": [
"*"
],
"searchRevInclude": [
"Coverage:patient",
"MedicationDispense:patient",
"Observation:patient"
]
}
]
}
]
}
Note the entries in each FHIR resource .. under "searchRevInclude" .
"searchRevInclude": [ "Coverage:patient", "MedicationDispense:patient", "Observation:patient" ]
So while I've added "search by Patient" as a FILTER, I have not done anything to "support rev-includes", but they are showing up in the CapablilityStatement.
If a resource provider (eg. Coverage) has no reverse include parameters on its @Search endpoint, the HAPI capability statement generates reverse includes for all possibilities rather than generating an empty reverse includes.
For example:
1) CoverageResourceProvider.java: @Search public IBundleProvider search(@OptionalParam(name="patient") TokenParam patient) { // No reverse includes ..
2) MedicationDispenseResourceProvider.java @Search public IBundleProvider search(@OptionalParam(name="patient") ReferenceParam patientRef) { // Not reverse searchable ...
Generated Statement:
"type": "Coverage", "profile": "http://hl7.org/fhir/StructureDefinition/Coverage", .. "searchRevInclude": [ "HealthcareService:location",
"MedicationDispense:patient", <-- Is this wrong? the resource does not define any reverse includes "OrganizationAffiliation:location", "PractitionerRole:location", "Procedure:patient" ],
Note that the capability statement does work properly when when your resource does define a reverse includes:
ConditionResourceProvider.java
@Search public IBundleProvider search( @IncludeParam(reverse = true, allow = {"Provenance:target"}) Set myParam)) { // Reverse searchable
...
"type": "Condition", "profile": "http://hl7.org/fhir/StructureDefinition/Condition", .. "searchRevInclude": [ "Provenance:target", <-- Correct! ],