Performance tweaks to the validator and get the DSTU2.1 endpoint working
with validation
This commit is contained in:
parent
685bd9345b
commit
7005cd52d9
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
ca.uhn.fhir.jpa.entity.ResourceTable/
|
||||
target/
|
||||
/bin
|
||||
nohup.out
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -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('.', '_')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue