Skip to content

Commit

Permalink
Merge branch 'main' into story/650/results-metadata-new-issue
Browse files Browse the repository at this point in the history
  • Loading branch information
saquino0827 committed Mar 26, 2024
2 parents 3481289 + a64eab3 commit 0fab3fd
Show file tree
Hide file tree
Showing 21 changed files with 800 additions and 2 deletions.
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ ext.jacoco_excludes = [
'**/slf4j/LocalLogger*',
'**/slf4j/DeployedLogger*',
'**/slf4j/LoggerHelper*',
'**/hapi/HapiFhirImplementation*',
'**/jjwt/JjwtEngine*',
'**/apache/ApacheClient*',
'**/azure/AzureSecrets*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import gov.hhs.cdc.trustedintermediary.etor.results.ResultResponse;
import gov.hhs.cdc.trustedintermediary.etor.results.ResultSender;
import gov.hhs.cdc.trustedintermediary.etor.results.SendResultUseCase;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader;
import gov.hhs.cdc.trustedintermediary.external.database.DatabasePartnerMetadataStorage;
import gov.hhs.cdc.trustedintermediary.external.database.DbDao;
import gov.hhs.cdc.trustedintermediary.external.database.PostgresDao;
Expand Down Expand Up @@ -123,6 +125,9 @@ public Map<HttpEndpoint, Function<DomainRequest, DomainResponse>> domainRegistra
PartnerMetadataOrchestrator.class, PartnerMetadataOrchestrator.getInstance());
ApplicationContext.register(
PartnerMetadataConverter.class, HapiPartnerMetadataConverter.getInstance());
// Validation rules
ApplicationContext.register(RuleLoader.class, RuleLoader.getInstance());
ApplicationContext.register(RuleEngine.class, RuleEngine.getInstance());

ApplicationContext.register(SendMessageHelper.class, SendMessageHelper.getInstance());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import gov.hhs.cdc.trustedintermediary.domainconnector.DomainRequest;
import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine;
import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirResource;
import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrder;
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException;
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir;
Expand All @@ -18,6 +20,7 @@ public class OrderController {
@Inject HapiFhir fhir;
@Inject Logger logger;
@Inject MetricMetadata metadata;
@Inject RuleEngine ruleEngine;

private OrderController() {}

Expand All @@ -28,6 +31,7 @@ public static OrderController getInstance() {
public Order<?> parseOrders(DomainRequest request) throws FhirParseException {
logger.logInfo("Parsing orders");
var fhirBundle = fhir.parseResource(request.getBody(), Bundle.class);
ruleEngine.validate(new HapiFhirResource(fhirBundle));
metadata.put(fhirBundle.getId(), EtorMetadataStep.RECEIVED_FROM_REPORT_STREAM);
return new HapiOrder(fhirBundle);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

/**
* This interface represents a FHIR resource. It's used as a wrapper to decouple dependency on third
* party libraries.
*
* @param <T> the type of the underlying resource
*/
public interface FhirResource<T> {
T getUnderlyingResource();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import java.util.List;

/**
* The Rule interface defines the structure for a rule in the rule engine. Each rule has a name,
* description, warning message, conditions, validations, and methods to check if a resource is
* valid and if the rule applies to a resource.
*/
public interface Rule {
String getName();

String getDescription();

/**
* Descriptive message when there's a rule violation Note: When implementing this method, make
* sure that no PII or PHI is included in the message!
*/
String getViolationMessage();

List<String> getConditions();

List<String> getValidations();

boolean isValid(FhirResource<?> resource);

boolean appliesTo(FhirResource<?> resource);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;

/** Manages the application of rules loaded from a definitions file using the RuleLoader. */
public class RuleEngine {

private static final RuleEngine INSTANCE = new RuleEngine();

final List<Rule> rules = new ArrayList<>();

@Inject Logger logger;
@Inject RuleLoader ruleLoader;

private RuleEngine() {}

public static RuleEngine getInstance() {
return INSTANCE;
}

public void unloadRules() {
rules.clear();
}

public void ensureRulesLoaded() {
if (rules.isEmpty()) {
loadRules();
}
}

private synchronized void loadRules() {
String fileName = "rule_definitions.json";
try (InputStream ruleDefinitionStream =
getClass().getClassLoader().getResourceAsStream(fileName)) {
assert ruleDefinitionStream != null;
var ruleStream =
new String(ruleDefinitionStream.readAllBytes(), StandardCharsets.UTF_8);
rules.addAll(ruleLoader.loadRules(ruleStream));
} catch (IOException | RuleLoaderException e) {
logger.logError("Failed to load rules definitions from: " + fileName, e);
}
}

public void validate(FhirResource<?> resource) {
logger.logDebug("Validating FHIR resource");
ensureRulesLoaded();
for (Rule rule : rules) {
if (rule.appliesTo(resource) && !rule.isValid(resource)) {
logger.logWarning("Rule violation: " + rule.getViolationMessage());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter;
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException;
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;

/** Manages the loading of rules from a definitions file. */
public class RuleLoader {
private static final RuleLoader INSTANCE = new RuleLoader();
@Inject Formatter formatter;

private RuleLoader() {}

public static RuleLoader getInstance() {
return INSTANCE;
}

public List<ValidationRule> loadRules(String ruleStream) throws RuleLoaderException {
try {
Map<String, List<ValidationRule>> jsonObj =
formatter.convertJsonToObject(ruleStream, new TypeReference<>() {});
return jsonObj.getOrDefault("rules", Collections.emptyList());
} catch (FormatterProcessingException e) {
throw new RuleLoaderException(
"Failed to load rules definitions for provided stream", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

/** Custom exception class use to catch RuleLoader exceptions */
public class RuleLoaderException extends Exception {

public RuleLoaderException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import gov.hhs.cdc.trustedintermediary.context.ApplicationContext;
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir;
import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import java.util.List;

/**
* Implements the Rule interface. It represents a rule with a name, description, warning message,
* conditions, and validations. It uses the HapiFhir engine to evaluate the conditions and
* validations.
*/
public class ValidationRule implements Rule {

private final Logger logger = ApplicationContext.getImplementation(Logger.class);
private final HapiFhir fhirEngine = ApplicationContext.getImplementation(HapiFhir.class);
private String name;
private String description;
private String violationMessage;
private List<String> conditions;
private List<String> validations;

/**
* Do not delete this constructor! It is used for JSON deserialization when loading rules from a
* file.
*/
public ValidationRule() {}

public ValidationRule(
String ruleName,
String ruleDescription,
String ruleWarningMessage,
List<String> ruleConditions,
List<String> ruleValidations) {
name = ruleName;
description = ruleDescription;
violationMessage = ruleWarningMessage;
conditions = ruleConditions;
validations = ruleValidations;
}

@Override
public String getName() {
return name;
}

@Override
public String getDescription() {
return description;
}

@Override
public String getViolationMessage() {
return violationMessage;
}

@Override
public List<String> getConditions() {
return conditions;
}

@Override
public List<String> getValidations() {
return validations;
}

@Override
public boolean isValid(FhirResource<?> resource) {
return validations.stream()
.allMatch(
validation -> {
try {
return fhirEngine.evaluateCondition(
resource.getUnderlyingResource(), validation);
} catch (Exception e) {
logger.logError(
"Rule ["
+ name
+ "]: "
+ "An error occurred while evaluating the validation: "
+ validation,
e);
return false;
}
});
}

@Override
public boolean appliesTo(FhirResource<?> resource) {
return conditions.stream()
.allMatch(
condition -> {
try {
return fhirEngine.evaluateCondition(
resource.getUnderlyingResource(), condition);
} catch (Exception e) {
logger.logError(
"Rule ["
+ name
+ "]: "
+ "An error occurred while evaluating the condition: "
+ condition,
e);
return false;
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gov.hhs.cdc.trustedintermediary.external.hapi;

import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource;
import org.hl7.fhir.instance.model.api.IBaseResource;

/** An implementation of {@link FhirResource} to use as a wrapper around HAPI FHIR IBaseResource */
public class HapiFhirResource implements FhirResource<IBaseResource> {

private final IBaseResource innerResource;

public HapiFhirResource(IBaseResource innerResource) {
this.innerResource = innerResource;
}

@Override
public IBaseResource getUnderlyingResource() {
return innerResource;
}
}
23 changes: 23 additions & 0 deletions etor/src/main/resources/rule_definitions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"rules": [ {
"name": "requiredReceiverId",
"description": "Message has required receiver id",
"violationMessage": "Message doesn't have required receiver id",
"conditions": [ ],
"validations": [
"Bundle.entry.resource.ofType(MessageHeader).destination.receiver.resolve().identifier.value.exists()"
]
},
{
"name": "requiredReceiverId",
"description": "ORM has required Card Number",
"violationMessage": "ORM doesn't have required Card Number",
"conditions": [
"Bundle.entry.resource.ofType(MessageHeader).event.code = 'O01'"
],
"validations": [
"Bundle.entry.resource.ofType(Observation).where(code.coding.code = '57723-9').value.coding.code.exists()"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gov.hhs.cdc.trustedintermediary

import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource

class FhirResourceMock<T> implements FhirResource<T> {

private T innerResource

FhirResourceMock(T innerResource) {
this.innerResource = innerResource
}

@Override
public T getUnderlyingResource() {
return innerResource
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ package gov.hhs.cdc.trustedintermediary.etor.orders
import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
import gov.hhs.cdc.trustedintermediary.domainconnector.DomainRequest
import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir
import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata
import org.hl7.fhir.r4.model.Bundle
import spock.lang.Specification

class OrderControllerTest extends Specification {
def ruleEngine = Mock(RuleEngine)

def setup() {
TestApplicationContext.reset()
TestApplicationContext.init()
TestApplicationContext.register(OrderController, OrderController.getInstance())
TestApplicationContext.register(MetricMetadata, Mock(MetricMetadata))
TestApplicationContext.register(RuleEngine, ruleEngine)
}

def "parseOrders happy path works"() {
Expand All @@ -33,6 +36,7 @@ class OrderControllerTest extends Specification {

then:
actualBundle == expectedBundle
(1.._) * ruleEngine.validate(_)
}

def "parseOrders registers a metadata step"() {
Expand Down
Loading

0 comments on commit 0fab3fd

Please sign in to comment.