Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature gic plugin #467

Merged
merged 56 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
6749f36
Updated formatting of SubmitDataProvider and added reference to DEQM …
Jan 30, 2022
59bb445
WIP: initial functional stub of $care-gaps operation
Jan 30, 2022
cb1bba7
Removed min and max operation param arguments as they are not used as…
Jan 30, 2022
dbd31a8
Merge branch 'feature-plugins' into feature-gic-plugin
Jan 30, 2022
68a0e89
Suppress excessive parameters warning
Jan 30, 2022
1945e4e
Merge branch 'bug-plugins' into feature-gic-plugin
Jan 30, 2022
c1d2e26
Update readme to include suggestion to install environment extensions
Jan 30, 2022
489ffe5
WIP: test not passing
Jan 31, 2022
f18c2ac
Fixed bugs in operation parameter validation helpers
Jan 31, 2022
0d8245a
Added Care Gaps operation parameter validation tests
Feb 1, 2022
a5b4186
Added tests for Operations validation for date validations
Feb 1, 2022
8a8f999
Added capability for operation parameters
Feb 1, 2022
8d7fbdc
Added retrieve of measures to care gaps
Feb 2, 2022
cf0c870
Make measures unique
Feb 2, 2022
0f2121c
Fix bug if one of the measure params is empty
Feb 2, 2022
851259c
Added bundle creation
Feb 5, 2022
b2e26b0
Abstracted ResourceSettings
Feb 5, 2022
9906b13
Fixed javadoc error
Feb 5, 2022
11c5f15
Merge branch 'feature-plugins' into feature-gic-plugin
Feb 5, 2022
47e71d9
Merge branch 'feature-quality-of-life-settings' into feature-gic-plugin
Feb 5, 2022
ddfeb0e
Merge branch 'bug-fix-build' into feature-gic-plugin
Feb 5, 2022
c366535
Merge branch 'bug-fix-build' into feature-gic-plugin
Feb 5, 2022
5b7d460
Merge the fix to suppress spot bugs and add care gaps plugin back int…
Feb 5, 2022
59bfd24
WIP abstracting builder
Feb 7, 2022
d4d2af4
Merge remote-tracking branch 'remotes/origin/feature-plugins' into fe…
Feb 8, 2022
75f115d
Add bundle builder
Feb 11, 2022
18d15c8
Initial working implementation of bundle builder
Feb 11, 2022
83a2605
WIP added composition to care gaps
Feb 12, 2022
b080970
WIP refactor builders for care gaps
Feb 12, 2022
3daca18
WIP refactor bundle builder
Feb 12, 2022
3d7105e
Refactored composition builder
Feb 12, 2022
db58b09
Merge remote-tracking branch 'origin/master' into feature-gic-plugin
Feb 12, 2022
206d008
Fixed bugs breaking care gaps tests
Feb 12, 2022
e0dd346
Refactor builders
Feb 13, 2022
7ec91ad
Add id and required constructors to builders
Feb 13, 2022
40b2f39
Add null protection to builders
Feb 14, 2022
5078d4d
Initial running MR creation (through MeasureEvaluateProvider)
Feb 19, 2022
fdd539b
WIP populating report
Feb 19, 2022
3069fcb
Added reporter to Care Gaps
Feb 20, 2022
92bf710
WIP harden the configuration checking
Feb 20, 2022
966de7f
Final version of validateConfiguration
Feb 20, 2022
9732685
WIP adding detected issues
Feb 21, 2022
c1ff57a
Added DomainResourceBuilder for extensions and modifier extensions
Feb 21, 2022
66e3820
Merge remote-tracking branch 'origin/master' into feature-gic-plugin
Feb 21, 2022
f4dca99
Refactor care gaps for clarity
Feb 21, 2022
95056e6
Refactor detectedIssue management to harden and for clarity
Feb 21, 2022
b8a5bfd
Suppress warnings as appropriate
Feb 21, 2022
7f089d1
Updating line endings in VS Code to match the git settings
Feb 21, 2022
db8f7fc
Move integration test settings to the SpringBootTest properties
Feb 21, 2022
33b2815
Protect the settings check for care gaps
Feb 21, 2022
2886f75
Fix bug in CompositionBuilder missing setting date
Feb 21, 2022
58f4157
Add testing to README
Feb 21, 2022
7029150
Added BackboneElementBuilder and CompositionSection builder
Feb 22, 2022
bfbd9f2
Added helper for urls
Feb 22, 2022
a53b8ab
Merge remote-tracking branch 'origin/bug-fix-cds-tests' into feature-…
Feb 22, 2022
d970709
Added configuration for the Care Gaps Composition Section Author
Feb 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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