Performance tweaks to the validator and get the DSTU2.1 endpoint working

with validation
This commit is contained in:
James Agnew 2016-01-04 17:57:00 -05:00
parent 685bd9345b
commit 7005cd52d9
13 changed files with 170 additions and 79 deletions

View File

@ -107,7 +107,11 @@ public class RuntimeResourceDefinition extends BaseRuntimeElementCompositeDefini
}
if (!UrlUtil.isValid(profile)) {
String profileWithUrl = theServerBase + "/Profile/" + profile;
String resourceName = "/StructureDefinition/";
if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) {
resourceName = "/Profile/";
}
String profileWithUrl = theServerBase + resourceName + profile;
if (UrlUtil.isValid(profileWithUrl)) {
return profileWithUrl;
}

View File

@ -1,49 +1,21 @@
package ca.uhn.fhir.rest.server.interceptor;
import java.nio.charset.Charset;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.ValidationResult;
/**
* This interceptor intercepts each outgoing response and if it contains a FHIR resource, validates that resource. The
* interceptor may be configured to run any validator modules, and will then add headers to the response or fail the
* request with an {@link UnprocessableEntityException HTTP 422 Unprocessable Entity}.
* This interceptor intercepts each outgoing response and if it contains a FHIR resource, validates that resource. The interceptor may be configured to run any validator modules, and will then add
* headers to the response or fail the request with an {@link UnprocessableEntityException HTTP 422 Unprocessable Entity}.
*/
public class ResponseValidatingInterceptor extends BaseValidatingInterceptor<IBaseResource> {
@ -54,12 +26,42 @@ public class ResponseValidatingInterceptor extends BaseValidatingInterceptor<IBa
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseValidatingInterceptor.class);
private Set<RestOperationTypeEnum> myExcludeOperationTypes;
/**
* Do not validate the following operations. A common use for this is to exclude {@link RestOperationTypeEnum#METADATA} so that this operation will execute as quickly as possible.
*/
public void addExcludeOperationType(RestOperationTypeEnum theOperationType) {
Validate.notNull(theOperationType, "theOperationType must not be null");
if (myExcludeOperationTypes == null) {
myExcludeOperationTypes = new HashSet<RestOperationTypeEnum>();
}
myExcludeOperationTypes.add(theOperationType);
}
@Override
ValidationResult doValidate(FhirValidator theValidator, IBaseResource theRequest) {
return theValidator.validateWithResult(theRequest);
}
@Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
validate(theResponseObject, theRequestDetails);
RestOperationTypeEnum operationType = theRequestDetails.getRestOperationType();
if (operationType != null && myExcludeOperationTypes != null && myExcludeOperationTypes.contains(operationType)) {
ourLog.trace("Operation type {} is excluded from validation", operationType);
return true;
}
validate(theResponseObject, theRequestDetails);
return true;
}
@Override
String provideDefaultResponseHeaderName() {
return DEFAULT_RESPONSE_HEADER_NAME;
}
/**
* Sets the name of the response header to add validation failures to
*
@ -71,14 +73,4 @@ public class ResponseValidatingInterceptor extends BaseValidatingInterceptor<IBa
super.setResponseHeaderName(theResponseHeaderName);
}
@Override
String provideDefaultResponseHeaderName() {
return DEFAULT_RESPONSE_HEADER_NAME;
}
@Override
ValidationResult doValidate(FhirValidator theValidator, IBaseResource theRequest) {
return theValidator.validateWithResult(theRequest);
}
}

View File

@ -1,3 +1,4 @@
ca.uhn.fhir.jpa.entity.ResourceTable/
target/
/bin
nohup.out

View File

@ -206,6 +206,13 @@ public class TestRestfulServer extends RestfulServer {
}
@Override
public void destroy() {
super.destroy();
ourLog.info("Server is shutting down");
myAppCtx.destroy();
}
/**
* The public server is deployed to http://fhirtest.uhn.ca and the JEE webserver
* where this FHIR server is deployed is actually fronted by an Apache HTTPd instance,

View File

@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu21;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu21;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
@ -120,6 +121,7 @@ public class TestDstu21Config extends BaseJavaConfigDstu21 {
responseValidator.addValidatorModule(myQuestionnaireResponseValidatorDstu21);
responseValidator.setResponseHeaderValueNoIssues("Validation did not detect any issues");
responseValidator.setFailOnSeverity(null);
responseValidator.addExcludeOperationType(RestOperationTypeEnum.METADATA);
return responseValidator;
}

View File

@ -13,7 +13,7 @@
<property name="servers">
<list>
<value>home_dev , DSTU2 , UHN/HAPI Server (DSTU2 FHIR) , http://fhirtest.uhn.ca/baseDstu2</value>
<value>home_21 , DSTU2 , UHN/HAPI Server (DSTU2.1 FHIR) , http://fhirtest.uhn.ca/baseDstu2.1</value>
<value>home_21 , DSTU2_1 , UHN/HAPI Server (DSTU2.1 FHIR) , http://fhirtest.uhn.ca/baseDstu2.1</value>
<value>home , DSTU1 , UHN/HAPI Server (DSTU1 FHIR) , http://fhirtest.uhn.ca/baseDstu1</value>
<value>hidev , DSTU2 , Health Intersections (DSTU2 FHIR) , http://fhir-dev.healthintersections.com.au/open</value>
<value>hi , DSTU1 , Health Intersections (DSTU1 FHIR) , http://fhir.healthintersections.com.au/open</value>

View File

@ -2,7 +2,9 @@ package org.hl7.fhir.dstu21.hapi.validation;
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.StringUtils;
import org.hl7.fhir.dstu21.formats.IParser;
@ -33,6 +35,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander, ValueSetExpanderFactory {
private final FhirContext myCtx;
private IValidationSupport myValidationSupport;
private Map<String, Resource> myFetchedResourceCache = new HashMap<String, Resource>();
public HapiWorkerContext(FhirContext theCtx, IValidationSupport theValidationSupport) {
myCtx = theCtx;
@ -58,7 +61,15 @@ public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander
if (myValidationSupport == null) {
return null;
} else {
return myValidationSupport.fetchResource(myCtx, theClass, theUri);
@SuppressWarnings("unchecked")
T retVal = (T) myFetchedResourceCache.get(theUri);
if (retVal == null) {
retVal = myValidationSupport.fetchResource(myCtx, theClass, theUri);
if (retVal != null) {
myFetchedResourceCache.put(theUri, retVal);
}
}
return retVal;
}
}
@ -132,7 +143,7 @@ public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander
}
for (ConceptSetComponent nextComposeConceptSet : theVs.getCompose().getInclude()) {
if (StringUtils.equals(theSystem, nextComposeConceptSet.getSystem())) {
if (theSystem == null || StringUtils.equals(theSystem, nextComposeConceptSet.getSystem())) {
for (ConceptReferenceComponent nextComposeCode : nextComposeConceptSet.getConcept()) {
ConceptDefinitionComponent conceptDef = new ConceptDefinitionComponent();
conceptDef.setCode(nextComposeCode.getCode());

View File

@ -21,6 +21,7 @@ import org.hl7.fhir.dstu21.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.dstu21.model.Enumerations.BindingStrength;
import org.hl7.fhir.dstu21.model.Extension;
import org.hl7.fhir.dstu21.model.HumanName;
import org.hl7.fhir.dstu21.model.IdType;
import org.hl7.fhir.dstu21.model.Identifier;
import org.hl7.fhir.dstu21.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.dstu21.model.OperationOutcome.IssueType;
@ -946,11 +947,15 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
WrapperElement we = resolve(ref, stack);
String ft;
if (we != null)
boolean hint;
if (we != null) {
ft = we.getResourceType();
else
hint = hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null, "Unable to determine type of target resource");
} else {
ft = tryParse(ref);
if (hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null, "Unable to determine type of target resource")) {
hint = hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft != null, "Unable to determine type of target resource in: {0}", ref);
}
if (hint) {
boolean ok = false;
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (TypeRefComponent type : container.getType()) {
@ -1493,18 +1498,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
private String tryParse(String ref) {
String[] parts = ref.split("\\/");
switch (parts.length) {
case 1:
IdType id = new IdType(ref);
if (!id.hasResourceType()) {
return null;
case 2:
return checkResourceType(parts[0]);
default:
if (parts[parts.length - 2].equals("_history"))
return checkResourceType(parts[parts.length - 4]);
else
return checkResourceType(parts[parts.length - 2]);
}
return checkResourceType(id.getResourceType());
}
private boolean typesAreAllReference(List<TypeRefComponent> theType) {

View File

@ -30,6 +30,7 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
import ca.uhn.fhir.util.PortUtil;
@ -111,6 +112,7 @@ public class ResponseValidatingInterceptorTest {
assertThat(status.toString(), not(containsString("X-HAPI-Response-Validation")));
}
@Test
public void testSearchJsonValidNoValidatorsSpecifiedDefaultMessage() throws Exception {
myInterceptor.setResponseHeaderValueNoIssues("NO ISSUES");
@ -185,6 +187,45 @@ public class ResponseValidatingInterceptorTest {
assertThat(status.toString(), containsString("X-HAPI-Response-Validation"));
}
@Test
public void testSkipEnabled() throws Exception {
IValidatorModule module = new FhirInstanceValidator();
myInterceptor.addValidatorModule(module);
myInterceptor.addExcludeOperationType(RestOperationTypeEnum.METADATA);
myInterceptor.setResponseHeaderValueNoIssues("No issues");
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/metadata");
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", status);
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(status.toString(), not(containsString("X-HAPI-Response-Validation")));
}
@Test
public void testSkipNotEnabled() throws Exception {
IValidatorModule module = new FhirInstanceValidator();
myInterceptor.addValidatorModule(module);
myInterceptor.setResponseHeaderValueNoIssues("No issues");
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/metadata");
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", status);
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(status.toString(), (containsString("X-HAPI-Response-Validation")));
}
@Test
public void testSearchXmlValidNoValidatorsSpecified() throws Exception {
Patient patient = new Patient();

View File

@ -9,10 +9,13 @@ import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu21.hapi.validation.DefaultProfileValidationSupport;
@ -95,7 +98,8 @@ public class FhirInstanceValidatorTest {
when(myMockSupport.fetchResource(any(FhirContext.class), any(Class.class), any(String.class))).thenAnswer(new Answer<IBaseResource>() {
@Override
public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[0], (Class<IBaseResource>) theInvocation.getArguments()[1], (String) theInvocation.getArguments()[2]);
IBaseResource retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[0], (Class<IBaseResource>) theInvocation.getArguments()[1],
(String) theInvocation.getArguments()[2]);
ourLog.info("fetchResource({}, {}) : {}", new Object[] { theInvocation.getArguments()[1], theInvocation.getArguments()[2], retVal });
return retVal;
}
@ -198,6 +202,28 @@ public class FhirInstanceValidatorTest {
assertEquals(output.toString(), 0, output.getMessages().size());
}
@Test
public void testValidateBigRawJsonResource() throws Exception {
InputStream stream = FhirInstanceValidatorTest.class.getResourceAsStream("/conformance.json.gz");
stream = new GZIPInputStream(stream);
String input = IOUtils.toString(stream);
long start = System.currentTimeMillis();
ValidationResult output = null;
int passes = 1;
for (int i = 0; i < passes; i++) {
ourLog.info("Pass {}", i+1);
output = myVal.validateWithResult(input);
}
long delay = System.currentTimeMillis() - start;
long per = delay / passes;
logResultsAndReturnAll(output);
ourLog.info("Took {} ms -- {}ms / pass", delay, per);
}
@Test
public void testValidateRawXmlResourceBadAttributes() {
// @formatter:off
@ -304,9 +330,12 @@ public class FhirInstanceValidatorTest {
@Test
public void testValidateResourceWithDefaultValuesetBadCode() {
String input = "<Observation xmlns=\"http://hl7.org/fhir\">\n" + " <status value=\"notvalidcode\"/>\n" + " <code>\n" + " <text value=\"No code here!\"/>\n" + " </code>\n" + "</Observation>";
String input = "<Observation xmlns=\"http://hl7.org/fhir\">\n" + " <status value=\"notvalidcode\"/>\n" + " <code>\n" + " <text value=\"No code here!\"/>\n" + " </code>\n"
+ "</Observation>";
ValidationResult output = myVal.validateWithResult(input);
assertEquals("The value provided is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status, and a code is required from this value set", output.getMessages().get(0).getMessage());
assertEquals(
"The value provided is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status, and a code is required from this value set",
output.getMessages().get(0).getMessage());
}
@Test
@ -319,7 +348,9 @@ public class FhirInstanceValidatorTest {
List<SingleValidationMessage> all = logResultsAndReturnAll(output);
assertEquals(1, all.size());
assertEquals("/f:Patient/f:identifier/f:type", all.get(0).getLocationString());
assertEquals("None of the codes provided are in the value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type, and a code should come from this value set unless it has no suitable code", all.get(0).getMessage());
assertEquals(
"None of the codes provided are in the value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type, and a code should come from this value set unless it has no suitable code",
all.get(0).getMessage());
assertEquals(ResultSeverityEnum.WARNING, all.get(0).getSeverity());
}

View File

@ -99,7 +99,7 @@ public class TesterConfig {
Validate.notBlank(nextSplit[3], "theServerBase can not be blank");
myIdToServerName.put(nextSplit[0].trim(), nextSplit[2].trim());
myIdToServerBase.put(nextSplit[0].trim(), nextSplit[3].trim());
myIdToFhirVersion.put(nextSplit[0].trim(), FhirVersionEnum.valueOf(nextSplit[1].trim().toUpperCase()));
myIdToFhirVersion.put(nextSplit[0].trim(), FhirVersionEnum.valueOf(nextSplit[1].trim().toUpperCase().replace('.', '_')));
}
}
}

View File

@ -106,6 +106,10 @@
Introduce custom @CoverageIgnore annotation to hapi-fhir-base in order to
remove dependency on cobertura during build and in runtime.
</action>
<action type="fix">
Server-generated conformance statements incorrectly used /Profile/ instead
of /StructureDefinition/ in URL links to structures.
</action>
</release>
<release version="1.3" date="2015-11-14">
<action type="add">