Initial commit

This commit is contained in:
Nick Goupinets 2021-04-22 09:48:40 -04:00
parent 19ff4862ec
commit 961f16a1be
7 changed files with 82 additions and 16 deletions

View File

@ -31,7 +31,7 @@ public interface IAddressValidator {
/** /**
* URL for validation results that should be placed on addresses * URL for validation results that should be placed on addresses
*/ */
public static final String ADDRESS_VALIDATION_EXTENSION_URL = "https://hapifhir.org/AddressValidation/"; public static final String ADDRESS_VALIDATION_EXTENSION_URL = "https://hapifhir.org/StructureDefinition/ext-validation-address-error";
/** /**
* Extension value confirming that address can be considered valid (it exists and can be traced to the building) * Extension value confirming that address can be considered valid (it exists and can be traced to the building)

View File

@ -37,6 +37,7 @@ import java.util.Properties;
public abstract class BaseRestfulValidator implements IAddressValidator { public abstract class BaseRestfulValidator implements IAddressValidator {
public static final String PROPERTY_SERVICE_KEY = "service.key"; public static final String PROPERTY_SERVICE_KEY = "service.key";
public static final String PROPERTY_SERVICE_ENDPOINT = "service.endpoint";
private static final Logger ourLog = LoggerFactory.getLogger(BaseRestfulValidator.class); private static final Logger ourLog = LoggerFactory.getLogger(BaseRestfulValidator.class);
@ -74,7 +75,7 @@ public abstract class BaseRestfulValidator implements IAddressValidator {
retVal.setRawResponse(responseBody); retVal.setRawResponse(responseBody);
try { try {
JsonNode response = new ObjectMapper().readTree(responseBody); JsonNode response = new ObjectMapper().readTree(responseBody);
ourLog.debug("Parsed address validator response {}", response); ourLog.debug("Parsed address validator response {}", response);
return getValidationResult(retVal, response, theFhirContext); return getValidationResult(retVal, response, theFhirContext);
} catch (Exception e) { } catch (Exception e) {
@ -97,4 +98,8 @@ public abstract class BaseRestfulValidator implements IAddressValidator {
protected String getApiKey() { protected String getApiKey() {
return getProperties().getProperty(PROPERTY_SERVICE_KEY); return getProperties().getProperty(PROPERTY_SERVICE_KEY);
} }
protected String getApiEndpoint() {
return getProperties().getProperty(PROPERTY_SERVICE_ENDPOINT);
}
} }

View File

@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -52,7 +53,7 @@ public class LoquateAddressValidator extends BaseRestfulValidator {
private static final String[] DUPLICATE_FIELDS_IN_ADDRESS_LINES = {"Locality", "AdministrativeArea", "PostalCode"}; private static final String[] DUPLICATE_FIELDS_IN_ADDRESS_LINES = {"Locality", "AdministrativeArea", "PostalCode"};
private static final String DATA_CLEANSE_ENDPOINT = "https://api.addressy.com/Cleansing/International/Batch/v1.00/json4.ws"; private static final String DEFAULT_DATA_CLEANSE_ENDPOINT = "https://api.addressy.com/Cleansing/International/Batch/v1.00/json4.ws";
private static final int MAX_ADDRESS_LINES = 8; private static final int MAX_ADDRESS_LINES = 8;
public LoquateAddressValidator(Properties theProperties) { public LoquateAddressValidator(Properties theProperties) {
@ -171,7 +172,13 @@ public class LoquateAddressValidator extends BaseRestfulValidator {
String requestBody = getRequestBody(theFhirContext, theAddress); String requestBody = getRequestBody(theFhirContext, theAddress);
HttpEntity<String> request = new HttpEntity<>(requestBody, headers); HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
return newTemplate().postForEntity(DATA_CLEANSE_ENDPOINT, request, String.class); return newTemplate().postForEntity(getApiEndpoint(), request, String.class);
}
@Override
protected String getApiEndpoint() {
String endpoint = super.getApiEndpoint();
return StringUtils.isEmpty(endpoint) ? DEFAULT_DATA_CLEANSE_ENDPOINT : endpoint;
} }
protected String getRequestBody(FhirContext theFhirContext, IBase... theAddresses) throws JsonProcessingException { protected String getRequestBody(FhirContext theFhirContext, IBase... theAddresses) throws JsonProcessingException {

View File

@ -56,7 +56,13 @@ public class MelissaAddressValidator extends BaseRestfulValidator {
@Override @Override
protected ResponseEntity<String> getResponseEntity(IBase theAddress, FhirContext theFhirContext) throws Exception { protected ResponseEntity<String> getResponseEntity(IBase theAddress, FhirContext theFhirContext) throws Exception {
Map<String, String> requestParams = getRequestParams(theAddress); Map<String, String> requestParams = getRequestParams(theAddress);
return newTemplate().getForEntity(GLOBAL_ADDRESS_VALIDATION_ENDPOINT, String.class, requestParams); return newTemplate().getForEntity(getApiEndpoint(), String.class, requestParams);
}
@Override
protected String getApiEndpoint() {
String endpoint = super.getApiEndpoint();
return StringUtils.isEmpty(endpoint) ? GLOBAL_ADDRESS_VALIDATION_ENDPOINT : endpoint;
} }
protected Map<String, String> getRequestParams(IBase theAddress) { protected Map<String, String> getRequestParams(IBase theAddress) {

View File

@ -27,7 +27,9 @@ import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ConfigLoader; import ca.uhn.fhir.rest.server.interceptor.ConfigLoader;
import ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator; import ca.uhn.fhir.util.ExtensionUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -46,12 +48,11 @@ public class FieldValidatingInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(FieldValidatingInterceptor.class); private static final Logger ourLog = LoggerFactory.getLogger(FieldValidatingInterceptor.class);
public static final String VALIDATION_DISABLED_HEADER = "HAPI-Field-Validation-Disabled"; public static final String VALIDATION_DISABLED_HEADER = "HAPI-Field-Validation-Disabled";
public static final String VALIDATION_EXTENSION_URL = "https://hapifhir.org/StructureDefinition/ext-validation-field-error";
private IAddressValidator myAddressValidator; public static final String PROPERTY_EXTENSION_URL = "validation.extension.url";
private Map<String, String> myConfig; private Map<String, String> myConfig;
public FieldValidatingInterceptor() { public FieldValidatingInterceptor() {
super(); super();
@ -84,20 +85,52 @@ public class FieldValidatingInterceptor {
FhirContext ctx = theRequest.getFhirContext(); FhirContext ctx = theRequest.getFhirContext();
IFhirPath fhirPath = ctx.newFhirPath(); IFhirPath fhirPath = ctx.newFhirPath();
for (Map.Entry<String, String> e : myConfig.entrySet()) { for (Map.Entry<String, String> e : myConfig.entrySet()) {
IValidator validator = getValidator(e.getValue()); IValidator validator = getValidator(e.getValue());
if (validator == null) {
continue;
}
List<IPrimitiveType> values = fhirPath.evaluate(theResource, e.getKey(), IPrimitiveType.class); List<IBase> fields = fhirPath.evaluate(theResource, e.getKey(), IBase.class);
for (IPrimitiveType value : values) { for (IBase field : fields) {
String valueAsString = value.getValueAsString();
if (!validator.isValid(valueAsString)) { List<IPrimitiveType> values = fhirPath.evaluate(field, "value", IPrimitiveType.class);
throw new IllegalArgumentException(String.format("Invalid resource %s", valueAsString)); boolean isValid = true;
for (IPrimitiveType value : values) {
String valueAsString = value.getValueAsString();
isValid = validator.isValid(valueAsString);
ourLog.debug("Field {} at path {} validated {}", value, e.getKey(), isValid);
if (!isValid) {
break;
}
} }
setValidationStatus(ctx, field, isValid);
} }
} }
} }
private void setValidationStatus(FhirContext ctx, IBase theBase, boolean isValid) {
if (isValid) {
ExtensionUtil.clearExtensionsByUrl(theBase, getValidationExtensionUrl());
} else {
IBaseExtension<?, ?> validationResultExtension = ExtensionUtil.addExtension(theBase, getValidationExtensionUrl());
ExtensionUtil.setExtension(ctx, theBase, getValidationExtensionUrl(), "boolean", Boolean.TRUE);
}
}
private String getValidationExtensionUrl() {
if (myConfig.containsKey(PROPERTY_EXTENSION_URL)) {
return myConfig.get(PROPERTY_EXTENSION_URL);
}
return VALIDATION_EXTENSION_URL;
}
private IValidator getValidator(String theValue) { private IValidator getValidator(String theValue) {
if (PROPERTY_EXTENSION_URL.equals(theValue)) {
return null;
}
if (ValidatorType.EMAIL.name().equals(theValue)) { if (ValidatorType.EMAIL.name().equals(theValue)) {
return new EmailValidator(); return new EmailValidator();
} }

View File

@ -1,3 +1,3 @@
{ {
"telecom.where(system='email').value" : "EMAIL" "telecom.where(system='email')" : "EMAIL"
} }

View File

@ -4,9 +4,12 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.ContactPoint; import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Person; import org.hl7.fhir.r4.model.Person;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays; import java.util.Arrays;
@ -19,6 +22,8 @@ import static org.mockito.Mockito.when;
class FieldValidatingInterceptorTest { class FieldValidatingInterceptorTest {
private static final Logger ourLog = LoggerFactory.getLogger(FieldValidatingInterceptorTest.class);
private FhirContext myFhirContext = FhirContext.forR4(); private FhirContext myFhirContext = FhirContext.forR4();
private FieldValidatingInterceptor myInterceptor = new FieldValidatingInterceptor(); private FieldValidatingInterceptor myInterceptor = new FieldValidatingInterceptor();
@ -61,12 +66,22 @@ class FieldValidatingInterceptorTest {
public void testInvalidEmailValidation() { public void testInvalidEmailValidation() {
Person person = new Person(); Person person = new Person();
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("@garbage"); person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("@garbage");
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("my@email.com");
try { try {
myInterceptor.handleRequest(newRequestDetails(), person); myInterceptor.handleRequest(newRequestDetails(), person);
fail();
} catch (Exception e) { } catch (Exception e) {
fail();
} }
ourLog.info("Resource looks like {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(person));
ContactPoint invalidEmail = person.getTelecomFirstRep();
assertTrue(invalidEmail.hasExtension());
assertEquals("true", invalidEmail.getExtensionString("https://hapifhir.org/StructureDefinition/ext-validation-field-error"));
ContactPoint validEmail = person.getTelecom().get(1);
assertFalse(validEmail.hasExtension());
} }
@Test @Test