hapifhir / hapi-fhir

🔥 HAPI FHIR - Java API for HL7 FHIR Clients and Servers
http://hapifhir.io
Apache License 2.0
2.04k stars 1.33k forks source link

Capability Statement Reverse Includes Issue #3042

Open bill-bishop-optum opened 3 years ago

bill-bishop-optum commented 3 years ago

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! ],

granadacoder commented 1 year 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.

  1. I don't have any ReverseIncludes in my Coverage world.
  2. MedicationDispense has nothing to do with Coverage...from a Include or RevInclude perspective.

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:

https://www.hl7.org/fhir/R4/coverage.html#search

https://www.hl7.org/fhir/R4/medicationdispense.html#search

granadacoder commented 1 year ago

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.