Skip to content

Commit

Permalink
Merge pull request #467 from DBCG/feature-gic-plugin
Browse files Browse the repository at this point in the history
Feature gic plugin
  • Loading branch information
JPercival authored Feb 22, 2022
2 parents fe5283f + d970709 commit 5c3d39a
Show file tree
Hide file tree
Showing 39 changed files with 312,082 additions and 213 deletions.
15 changes: 12 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,30 @@
"Alphora",
"autoconfiguration",
"autoconfigure",
"CAREGAP",
"Checkstyle",
"classpath",
"Codeable",
"contentgroup",
"DEQM",
"Eicrs",
"mgsc",
"drawio",
"Dstu",
"Eicrs",
"FHIR",
"Gson",
"Javadoc",
"jpaserver",
"loinc",
"mgsc",
"mvnw",
"numer",
"opencds",
"pdmp",
"reindex",
"Servlet",
"snomed",
"springframework",
"valueset",
"valuesets",
"Zulip"
],
Expand All @@ -57,6 +66,6 @@
"**/.settings": true,
"**/.factorypath": true
},
"files.eol": "\n",
"files.eol": "\r\n",
"editor.formatOnSave": true
}
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ Currently the cqf-ruler recognizes three types of plugin contributions:

The plugin system is very simple and naive. Plugins are expected to be well-behaved, and not contribute any beans that may be invalid for the current server's configuration. This includes but is not limited to, multiple versions of plugins, mismatched FHIR versions, operation overrides, etc.

#### Plugin Testing

Integration Tests

Configuration settings for integration tests should be implemented in the `properties` element of the `SpringBootTest` annotation.

Example:

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { HelloWorldProviderIT.class,
HelloWorldConfig.class }, properties = {
"hapi.fhir.fhir_version=r4",
"hello.world.message=Howdy"
})
```

## Coding Conventions

The CQF Project has adopted an over-arching goal to contribute back to HAPI.
Expand All @@ -110,6 +126,22 @@ To this end:
* The CQF Ruler project has adopted the HAPI Coding Conventions: <https://github.com/hapifhir/hapi-fhir/wiki/Contributing>
* Plugins should generally use the "hapi.fhir" prefix for configuration properties

### Extensions

Where possible, environment extensions have been recommended. Please ensure the recommended extensions for your environment have been installed.

### Style

The CQF Project uses Checkstyle to enforce the coding standard. This will cause a build failure in the event of a Checkstyle error. Visit <https://checkstyle.sourceforge.io/> for more info.

Results of Checkstyle errors can be found in the corresponding `checkstyle-result.xml` file.

### Javadoc

The CQF Project has strict checking for Javadoc enabled. This will cause a build failure in the event of a Javadoc warning. Visit <https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html> for more info.

Results of Javadoc can be found in the output of the build.

### Utility Guidelines

#### Types of Utilities
Expand All @@ -136,7 +168,7 @@ or, if you put unrelated code into the class, you might end up with something li

If the code doesn't read clearly after you've added an utility, consider that it may not be in the right place.

In general, all the functions for this type of utility should be `static`. No internal state should be maintained (`static final`, or immutable, state is ok). If you final that your utility class contains mutable state, consider an alternate design.
In general, all the functions for this type of utility should be `static`. No internal state should be maintained (`static final`, or immutable, state is ok). If you find that your utility class contains mutable state, consider an alternate design.

Examples

Expand All @@ -145,7 +177,7 @@ Examples

#### Behavior Specific Utilities

If there is behavior you'd like to share across many classes, model that as an interface and use a name that follows the pattern `"ThingDoer"`. For example, all the classes that access a database might be `DatabaseReader`. Use `default` interface implementations to write logic that can be shared many places. The interfaces themselves shouldn't have mutable state (again `static final` is ok). If it's necessary for the for shared logic to have access to state, model that as an method without a default implementation. For example:
If there is behavior you'd like to share across many classes, model that as an interface and use a name that follows the pattern `"ThingDoer"`. For example, all the classes that access a database might be `DatabaseReader`. Use `default` interface implementations to write logic that can be shared many places. The interfaces themselves shouldn't have mutable state (again `static final` is ok). If it's necessary for the shared logic to have access to state, model that as an method without a default implementation. For example:

```java
interface DatabaseReader {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.opencds.cqf.ruler.behavior;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.HashSet;
import java.util.Set;

public interface ConfigurationUser {
static final ConfigurationSet configurations = new ConfigurationSet();

public abstract void validateConfiguration();

default ConfigurationUser validateConfiguration(Object theConfiguration, boolean theExpression, String theMessage) {
if (configurationValid(theConfiguration)) {
return this;
}

try {
checkNotNull(theConfiguration);
checkArgument(theExpression, theMessage);
} catch (Exception e) {
setConfigurationInvalid(theConfiguration);
throw e;
}

return this;
}

default void setConfigurationValid(Object theConfiguration) {
configurations.getConfigurations().add(theConfiguration);
}

default void setConfigurationInvalid(Object theConfiguration) {
configurations.getConfigurations().remove(theConfiguration);
}

default boolean configurationValid(Object theConfiguration) {
return configurations.getConfigurations().contains(theConfiguration);
}

class ConfigurationSet {
private final Set<Object> configurations = new HashSet<>();

public Set<Object> getConfigurations() {
return this.configurations;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.opencds.cqf.ruler.behavior.r4;

import java.util.HashMap;
import java.util.Map;

import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.ruler.behavior.DaoRegistryUser;
import org.opencds.cqf.ruler.behavior.IdCreator;
import org.opencds.cqf.ruler.utility.Ids;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public interface MeasureReportUser extends DaoRegistryUser, IdCreator {
static final Logger ourLog = LoggerFactory.getLogger(ParameterUser.class);

public static final String MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM = "http://terminology.hl7.org/CodeSystem/measure-improvement-notation";
public static final String MEASUREREPORT_MEASURE_POPULATION_SYSTEM = "http://terminology.hl7.org/CodeSystem/measure-population";

public default Map<String, Resource> getEvaluatedResources(org.hl7.fhir.r4.model.MeasureReport report) {
Map<String, Resource> resources = new HashMap<>();
getEvaluatedResources(report, resources);

return resources;
}

public default MeasureReportUser getEvaluatedResources(org.hl7.fhir.r4.model.MeasureReport report,
Map<String, Resource> resources) {
report.getEvaluatedResource().forEach(evaluatedResource -> {
IIdType resourceId = evaluatedResource.getReferenceElement();
if (resourceId.getResourceType() == null || resources.containsKey(Ids.simple(resourceId))) {
return;
}
IBaseResource resourceBase = read(resourceId);
if (resourceBase instanceof Resource) {
Resource resource = (Resource) resourceBase;
resources.put(Ids.simple(resourceId), resource);
}
});

return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.opencds.cqf.ruler.behavior.r4;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.NotImplementedException;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Group;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.opencds.cqf.ruler.behavior.DaoRegistryUser;
import org.opencds.cqf.ruler.behavior.IdCreator;
import org.opencds.cqf.ruler.utility.Searches;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ca.uhn.fhir.rest.api.server.RequestDetails;

public interface ParameterUser extends DaoRegistryUser, IdCreator {
static final Logger ourLog = LoggerFactory.getLogger(ParameterUser.class);

// TODO: document all these
// TODO: unit test all these
void validateParameters(RequestDetails theRequestDetails);

default List<Measure> getMeasures(List<String> measureIds, List<String> measureIdentifiers,
List<CanonicalType> measureCanonicals) {
boolean hasMeasureIds = measureIds != null && !measureIds.isEmpty();
boolean hasMeasureIdentifiers = measureIdentifiers != null && !measureIdentifiers.isEmpty();
boolean hasMeasureUrls = measureCanonicals != null && !measureCanonicals.isEmpty();
if (!hasMeasureIds && !hasMeasureIdentifiers && !hasMeasureUrls) {
return Collections.emptyList();
}

List<Measure> measureList = new ArrayList<>();

if (hasMeasureIds) {
measureList.addAll(search(Measure.class, Searches.byIds(measureIds)).getAllResourcesTyped());
}

// TODO: implement searching by measure identifiers
if (hasMeasureIdentifiers) {
throw new NotImplementedException();
// measureList.addAll(search(Measure.class,
// Searches.byIdentifiers(measureIdentifiers)).getAllResourcesTyped());
}

if (hasMeasureUrls) {
measureList.addAll(search(Measure.class, Searches.byCanonicals(measureCanonicals)).getAllResourcesTyped());
}

Map<String, Measure> result = new HashMap<>();
measureList.forEach(measure -> result.putIfAbsent(measure.getUrl(), measure));

return new ArrayList<>(result.values());
}

// TODO: replace this with version from the evaluator?
default List<Patient> getPatientListFromSubject(String subject) {
if (subject.startsWith("Patient/")) {
return Collections.singletonList(ensurePatient(subject));
} else if (subject.startsWith("Group/")) {
return getPatientListFromGroup(subject);
}

ourLog.info("Subject member was not a Patient or a Group, so skipping. \n{}", subject);
return Collections.emptyList();
}

// TODO: replace this with version from the evaluator?
default List<Patient> getPatientListFromGroup(String subjectGroupId) {
List<Patient> patientList = new ArrayList<>();

Group group = read(newId(subjectGroupId));
if (group == null) {
throw new IllegalArgumentException("Could not find Group: " + subjectGroupId);
}

group.getMember().forEach(member -> {
Reference reference = member.getEntity();
if (reference.getReferenceElement().getResourceType().equals("Patient")) {
Patient patient = ensurePatient(reference.getReference());
patientList.add(patient);
} else if (reference.getReferenceElement().getResourceType().equals("Group")) {
patientList.addAll(getPatientListFromGroup(reference.getReference()));
} else {
ourLog.info("Group member was not a Patient or a Group, so skipping. \n{}", reference.getReference());
}
});

return patientList;
}

// TODO: replace this with version from the evaluator?
default Patient ensurePatient(String patientRef) {
Patient patient = read(newId(patientRef));
if (patient == null) {
throw new IllegalArgumentException("Could not find Patient: " + patientRef);
}

return patient;
}
}
Loading

0 comments on commit 5c3d39a

Please sign in to comment.