This commit is contained in:
Lloyd McKenzie 2020-12-04 08:05:48 -07:00
commit d8df660c33
88 changed files with 31271 additions and 34419 deletions

View File

@ -6,7 +6,7 @@
### CI/CD
All intergration and delivery done on Azure pipelines. Azure project can be viewed [here][Link-AzureProject].
All integration and delivery done on Azure pipelines. Azure project can be viewed [here][Link-AzureProject].
### Current Versions
| Project | Current Release | Latest SNAPSHOT |

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -3,6 +3,7 @@ package org.hl7.fhir.convertors.conv40_50;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.StructureMap.StructureMapGroupTypeMode;
import java.util.stream.Collectors;
@ -220,8 +221,11 @@ public class StructureMap40_50 extends VersionConvertor_40_50 {
tgt.setNameElement(convertId(src.getNameElement()));
if (src.hasExtends())
tgt.setExtendsElement(convertId(src.getExtendsElement()));
if (src.hasTypeMode())
if (src.hasTypeMode()) {
tgt.setTypeModeElement(convertStructureMapGroupTypeMode(src.getTypeModeElement()));
} else {
tgt.setTypeMode(StructureMapGroupTypeMode.NONE);
}
if (src.hasDocumentation())
tgt.setDocumentationElement(convertString(src.getDocumentationElement()));
for (org.hl7.fhir.r5.model.StructureMap.StructureMapGroupInputComponent t : src.getInput()) tgt.addInput(convertStructureMapGroupInputComponent(t));

View File

@ -36,6 +36,7 @@ import java.net.URISyntaxException;
import org.hl7.fhir.r5.model.FhirPublication;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
public class TerminologyClientFactory {
@ -53,6 +54,20 @@ public class TerminologyClientFactory {
}
}
public static TerminologyClient makeClient(String url, String v) throws URISyntaxException {
if (v == null)
return new TerminologyClientR5(checkEndsWith("/r4", url));
v = VersionUtilities.getMajMin(v);
switch (v) {
case "1.0": return new TerminologyClientR2(checkEndsWith("/r2", url));
case "1.4": return new TerminologyClientR3(checkEndsWith("/r3", url)); // r3 is the least worst match
case "3.0": return new TerminologyClientR3(checkEndsWith("/r3", url));
case "4.0": return new TerminologyClientR4(checkEndsWith("/r4", url));
case "4.5": return new TerminologyClientR5(checkEndsWith("/r4", url)); // r4 for now, since the terminology is currently the same
default: throw new Error("The version "+v.toString()+" is not currently supported");
}
}
private static String checkEndsWith(String term, String url) {
if (url.endsWith(term))
return url;

View File

@ -36,15 +36,19 @@ import java.util.Map;
import org.hl7.fhir.convertors.VersionConvertor_10_50;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.dstu2.model.Resource;
import org.hl7.fhir.dstu2.utils.client.FHIRToolingClient;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.TerminologyServiceException;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
public class TerminologyClientR2 implements TerminologyClient {
@ -124,5 +128,27 @@ public class TerminologyClientR2 implements TerminologyClient {
return (Bundle) VersionConvertor_10_50.convertResource(client.transaction((org.hl7.fhir.dstu2.model.Bundle) VersionConvertor_10_50.convertResource(batch)));
}
@Override
public CanonicalResource read(String type, String id) {
Class<Resource> t;
try {
t = (Class<Resource>) Class.forName("org.hl7.fhir.dstu2.model."+type);// todo: do we have to deal with any resource renaming? Use cases are limited...
} catch (ClassNotFoundException e) {
throw new FHIRException("Unable to fetch resources of type "+type+" in R2");
}
org.hl7.fhir.dstu2.model.Resource r2 = client.read(t, id);
if (r2 == null) {
throw new FHIRException("Unable to fetch resource "+Utilities.pathURL(getAddress(), type, id));
}
org.hl7.fhir.r5.model.Resource r5 = VersionConvertor_10_50.convertResource(r2);
if (r5 != null) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 (internal representation)");
}
if (!(r5 instanceof CanonicalResource)) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 canonical resource (internal representation)");
}
return (CanonicalResource) r5;
}
}

View File

@ -34,17 +34,21 @@ package org.hl7.fhir.convertors.txClient;
import java.net.URISyntaxException;
import java.util.Map;
import org.hl7.fhir.convertors.VersionConvertor_10_50;
import org.hl7.fhir.convertors.VersionConvertor_30_50;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.utils.client.FHIRToolingClient;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
public class TerminologyClientR3 implements TerminologyClient {
@ -124,5 +128,27 @@ public class TerminologyClientR3 implements TerminologyClient {
return (Bundle) VersionConvertor_30_50.convertResource(client.transaction((org.hl7.fhir.dstu3.model.Bundle) VersionConvertor_30_50.convertResource(batch, false)), false);
}
@Override
public CanonicalResource read(String type, String id) {
Class<Resource> t;
try {
t = (Class<Resource>) Class.forName("org.hl7.fhir.dstu3.model."+type);// todo: do we have to deal with any resource renaming? Use cases are limited...
} catch (ClassNotFoundException e) {
throw new FHIRException("Unable to fetch resources of type "+type+" in R2");
}
org.hl7.fhir.dstu3.model.Resource r3 = client.read(t, id);
if (r3 == null) {
throw new FHIRException("Unable to fetch resource "+Utilities.pathURL(getAddress(), type, id));
}
org.hl7.fhir.r5.model.Resource r5 = VersionConvertor_30_50.convertResource(r3, false);
if (r5 != null) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 (internal representation)");
}
if (!(r5 instanceof CanonicalResource)) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 canonical resource (internal representation)");
}
return (CanonicalResource) r5;
}
}

View File

@ -34,17 +34,21 @@ package org.hl7.fhir.convertors.txClient;
import java.net.URISyntaxException;
import java.util.Map;
import org.hl7.fhir.convertors.VersionConvertor_30_50;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.convertors.conv40_50.TerminologyCapabilities40_50;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
public class TerminologyClientR4 implements TerminologyClient {
@ -124,4 +128,26 @@ public class TerminologyClientR4 implements TerminologyClient {
return (Bundle) VersionConvertor_40_50.convertResource(client.transaction((org.hl7.fhir.r4.model.Bundle) VersionConvertor_40_50.convertResource(batch)));
}
@Override
public CanonicalResource read(String type, String id) {
Class<Resource> t;
try {
t = (Class<Resource>) Class.forName("org.hl7.fhir.r4.model."+type);// todo: do we have to deal with any resource renaming? Use cases are limited...
} catch (ClassNotFoundException e) {
throw new FHIRException("Unable to fetch resources of type "+type+" in R2");
}
org.hl7.fhir.r4.model.Resource r4 = client.read(t, id);
if (r4 == null) {
throw new FHIRException("Unable to fetch resource "+Utilities.pathURL(getAddress(), type, id));
}
org.hl7.fhir.r5.model.Resource r5 = VersionConvertor_40_50.convertResource(r4);
if (r5 != null) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 (internal representation)");
}
if (!(r5 instanceof CanonicalResource)) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 canonical resource (internal representation)");
}
return (CanonicalResource) r5;
}
}

View File

@ -34,8 +34,11 @@ package org.hl7.fhir.convertors.txClient;
import java.net.URISyntaxException;
import java.util.Map;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.Parameters;
@ -44,6 +47,7 @@ import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.client.FHIRToolingClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
public class TerminologyClientR5 implements TerminologyClient {
@ -116,4 +120,22 @@ public class TerminologyClientR5 implements TerminologyClient {
return client.transaction(batch);
}
@Override
public CanonicalResource read(String type, String id) {
Class<Resource> t;
try {
t = (Class<Resource>) Class.forName("org.hl7.fhir.r5.model."+type);// todo: do we have to deal with any resource renaming? Use cases are limited...
} catch (ClassNotFoundException e) {
throw new FHIRException("Unable to fetch resources of type "+type+" in R5");
}
org.hl7.fhir.r5.model.Resource r5 = client.read(t, id);
if (r5 != null) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 (internal representation)");
}
if (!(r5 instanceof CanonicalResource)) {
throw new FHIRException("Unable to convert resource "+Utilities.pathURL(getAddress(), type, id)+" to R5 canonical resource (internal representation)");
}
return (CanonicalResource) r5;
}
}

View File

@ -0,0 +1,27 @@
package org.hl7.fhir.convertors.conv10_30;
import org.hl7.fhir.convertors.VersionConvertorAdvisor30;
import org.hl7.fhir.convertors.VersionConvertor_10_30;
import org.hl7.fhir.convertors.loaders.R2ToR3Loader;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.stream.Stream;
public class AdministrativeGender10_30Test {
@Test
@DisplayName("Test 10_30 extension present, value is not")
public void testMedicationRequestConversion() throws IOException {
InputStream dstu2_input = this.getClass().getResourceAsStream("/administrative_gender_null.json");
org.hl7.fhir.dstu2.model.Patient dstu2 = (org.hl7.fhir.dstu2.model.Patient) new org.hl7.fhir.dstu2.formats.JsonParser().parse(dstu2_input);
VersionConvertorAdvisor30 advisor = new R2ToR3Loader();
org.hl7.fhir.dstu3.model.Resource stu_actual = VersionConvertor_10_30.convertResource(dstu2, advisor);
}
}

View File

@ -0,0 +1,24 @@
package org.hl7.fhir.convertors.conv10_30;
import org.hl7.fhir.convertors.VersionConvertor_10_30;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class TimingRepeatComponent10_30Test {
@Test
@DisplayName("Issue #383 - Test 10_30 TimingRepeatComponent with Timing.when as null")
public void testMedicationRequestConversion() {
final int SET_COUNT = 11;
org.hl7.fhir.dstu2.model.Timing.TimingRepeatComponent src = new org.hl7.fhir.dstu2.model.Timing.TimingRepeatComponent();
src.setCount(SET_COUNT);
org.hl7.fhir.dstu3.model.Timing.TimingRepeatComponent tgt = VersionConvertor_10_30.convertTimingRepeatComponent(src);
Assertions.assertEquals(SET_COUNT, tgt.getCount(), "Count field not preserved through version conversion.");
Assertions.assertFalse(tgt.hasWhen(), "hasWhen() should return false for this conversion.");
Assertions.assertTrue(tgt.getWhen().isEmpty(), "When no _when time_ is provided, getWhen() should return an empty list.");
}
}

View File

@ -0,0 +1,94 @@
{
"resourceType": "Patient",
"id": "12743884",
"meta": {
"versionId": "0",
"lastUpdated": "2020-09-15T06:35:01.000Z"
},
"text": {
"status": "generated",
"div": "<div><p><b>Patient</b></p><p><b>Name</b>: Dawg, Joel</p><p><b>DOB</b>: Nov 11, 1991</p><p><b>Status</b>: Active</p></div>"
},
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://hl7.org/fhir/v2/0203",
"code": "MR",
"display": "Medical record number",
"userSelected": false
}
],
"text": "MRN"
},
"system": "urn:oid:2.16.840.1.113883.6.1000",
"value": "7690",
"_value": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/rendered-value",
"valueString": "00000007690"
}
]
},
"period": {
"start": "2020-09-15T06:35:01.000Z"
}
},
{
"use": "usual",
"type": {
"text": "Military Id"
},
"system": "urn:oid:2.16.840.1.113883.3.42.10001.100001.12",
"value": "10050007740",
"_value": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/rendered-value",
"valueString": "10050007740"
}
]
},
"period": {
"start": "2020-09-15T06:35:01.000Z"
}
}
],
"active": true,
"name": [
{
"use": "official",
"text": "Dawg, Joel",
"family": [
"Dawg"
],
"given": [
"Joel"
]
}
],
"telecom": [
{
"system": "phone",
"value": "3075557575",
"use": "home"
},
{
"system": "email",
"value": "amitabhp@mindfiresolutions.com",
"use": "work"
}
],
"_gender": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}
]
},
"birthDate": "1991-11-11"
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -516,7 +516,7 @@ public class FHIRToolingClient {
return p_out;
}
} catch (Exception e) {
handleException("Error performing operation '"+name+"' with parameters " + ps, e);
handleException("Error performing operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);
}
return null;
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -517,7 +517,7 @@ public class FHIRToolingClient {
return p_out;
}
} catch (Exception e) {
handleException("Error performing operation '"+name+"' with parameters " + ps, e);
handleException("Error performing operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);
}
return null;
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -504,7 +504,7 @@ public class FHIRToolingClient {
return p_out;
}
} catch (Exception e) {
handleException("Error performing operation '"+name+"' with parameters " + ps, e);
handleException("Error performing operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);
}
return null;
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -507,7 +507,7 @@ public class FHIRToolingClient {
return p_out;
}
} catch (Exception e) {
handleException("Error performing operation '"+name+"' with parameters " + ps, e);
handleException("Error performing operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);
}
return null;
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -101,6 +101,11 @@
<artifactId>httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
</dependency>
<!-- Test dependencies -->
<dependency>
@ -119,6 +124,13 @@
<version>${validator_test_case_version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.9.0</version>
<scope>test</scope>
</dependency>
<!-- JUnit Jupiter -->
<dependency>

View File

@ -4141,11 +4141,13 @@ public class ProfileUtilities extends TranslatingUtilities {
first = false;
}
}
if (first)
if (first) {
c.getPieces().add(gen.new Piece(null, "Any", null));
}
if (ADD_REFERENCE_TO_TABLE)
if (ADD_REFERENCE_TO_TABLE) {
c.getPieces().add(gen.new Piece(null, ")", null));
}
} else {
StructureDefinition sd = context.fetchTypeDefinition(t);
@ -5617,6 +5619,7 @@ public class ProfileUtilities extends TranslatingUtilities {
return;
Map<String, String> idList = new HashMap<String, String>();
Map<String, String> replacedIds = new HashMap<String, String>();
SliceList sliceInfo = new SliceList();
// first pass, update the element ids
@ -5643,6 +5646,9 @@ public class ProfileUtilities extends TranslatingUtilities {
}
}
String bs = b.toString();
if (ed.hasId()) {
replacedIds.put(ed.getId(), ed.getPath());
}
ed.setId(bs);
if (idList.containsKey(bs)) {
if (exception || messages == null) {
@ -5653,7 +5659,11 @@ public class ProfileUtilities extends TranslatingUtilities {
idList.put(bs, ed.getPath());
if (ed.hasContentReference() && ed.getContentReference().startsWith("#")) {
String s = ed.getContentReference();
ed.setContentReference("http://hl7.org/fhir/StructureDefinition/"+type+s);
if (replacedIds.containsKey(s.substring(1))) {
ed.setContentReference("http://hl7.org/fhir/StructureDefinition/"+type+"#"+replacedIds.get(s.substring(1)));
} else {
ed.setContentReference("http://hl7.org/fhir/StructureDefinition/"+type+s);
}
}
}
// second path - fix up any broken path based id references

View File

@ -193,6 +193,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
private boolean allowLoadingDuplicates;
protected TerminologyClient txClient;
private Set<String> codeSystemsUsed = new HashSet<>();
protected ToolingClientLogger txLog;
private TerminologyCapabilities txcaps;
private boolean canRunWithoutTerminology;
@ -597,10 +598,17 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
Parameters p = expParameters.copy();
p.setParameter("includeDefinition", false);
p.setParameter("excludeNested", !hierarchical);
if (isTxCaching && cacheId != null) {
boolean cached = addDependentResources(p, vs);
if (cached) {
p.addParameter().setName("cache-id").setValue(new StringType(cacheId));
}
addDependentResources(p, vs);
for (ConceptSetComponent incl : vs.getCompose().getInclude()) {
codeSystemsUsed.add(incl.getSystem());
}
for (ConceptSetComponent incl : vs.getCompose().getExclude()) {
codeSystemsUsed.add(incl.getSystem());
}
if (noTerminologyServer) {
return new ValueSetExpansionOutcome(formatMessage(I18nConstants.ERROR_EXPANDING_VALUESET_RUNNING_WITHOUT_TERMINOLOGY_SERVICES), TerminologyServiceErrorClass.NOSERVICE);
@ -640,6 +648,12 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
if (!vs.hasUrl()) {
throw new Error(formatMessage(I18nConstants.NO_VALUE_SET_IN_URL));
}
for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
codeSystemsUsed.add(inc.getSystem());
}
for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
codeSystemsUsed.add(inc.getSystem());
}
CacheToken cacheToken = txCache.generateExpandToken(vs, heirarchical);
ValueSetExpansionOutcome res;
@ -672,10 +686,10 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
}
// if that failed, we try to expand on the server
if (isTxCaching && cacheId != null) {
if (addDependentResources(p, vs)) {
p.addParameter().setName("cache-id").setValue(new StringType(cacheId));
}
addDependentResources(p, vs);
if (noTerminologyServer) {
return new ValueSetExpansionOutcome(formatMessage(I18nConstants.ERROR_EXPANDING_VALUESET_RUNNING_WITHOUT_TERMINOLOGY_SERVICES), TerminologyServiceErrorClass.NOSERVICE, allErrors);
}
@ -736,6 +750,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
// 3rd pass: hit the server
for (CodingValidationRequest t : codes) {
t.setCacheToken(txCache != null ? txCache.generateValidationToken(options, t.getCoding(), vs) : null);
codeSystemsUsed.add(t.getCoding().getSystem());
if (txCache != null) {
t.setResult(txCache.getValidation(t.getCacheToken()));
}
@ -808,6 +823,9 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
BundleEntryComponent r = resp.getEntry().get(i);
if (r.getResource() instanceof Parameters) {
t.setResult(processValidationResult((Parameters) r.getResource()));
if (txCache != null) {
txCache.cacheValidation(t.getCacheToken(), t.getResult(), TerminologyCache.PERMANENT);
}
} else {
t.setResult(new ValidationResult(IssueSeverity.ERROR, getResponseText(r.getResource())).setTxLink(txLog == null ? null : txLog.getLastId()));
}
@ -828,6 +846,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
options = ValidationOptions.defaults();
}
codeSystemsUsed.add(code.getSystem());
CacheToken cacheToken = txCache != null ? txCache.generateValidationToken(options, code, vs) : null;
ValidationResult res = null;
if (txCache != null) {
@ -902,6 +921,9 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
if (res != null) {
return res;
}
for (Coding c : code.getCoding()) {
codeSystemsUsed.add(c.getSystem());
}
if (options.isUseClient()) {
// ok, first we try to validate locally
@ -939,8 +961,14 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
}
private ValidationResult validateOnServer(ValueSet vs, Parameters pin) throws FHIRException {
if (isTxCaching && cacheId != null) {
pin.addParameter().setName("cache-id").setValue(new StringType(cacheId));
boolean cache = false;
if (vs != null) {
for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
codeSystemsUsed.add(inc.getSystem());
}
for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
codeSystemsUsed.add(inc.getSystem());
}
}
if (vs != null) {
if (isTxCaching && cacheId != null && cached.contains(vs.getUrl()+"|"+vs.getVersion())) {
@ -949,8 +977,12 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
pin.addParameter().setName("valueSet").setResource(vs);
cached.add(vs.getUrl()+"|"+vs.getVersion());
}
cache = true;
addDependentResources(pin, vs);
}
if (cache) {
pin.addParameter().setName("cache-id").setValue(new StringType(cacheId));
}
for (ParametersParameterComponent pp : pin.getParameter()) {
if (pp.getName().equals("profile")) {
throw new Error(formatMessage(I18nConstants.CAN_ONLY_SPECIFY_PROFILE_IN_THE_CONTEXT));
@ -975,22 +1007,26 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
return processValidationResult(pOut);
}
private void addDependentResources(Parameters pin, ValueSet vs) {
private boolean addDependentResources(Parameters pin, ValueSet vs) {
boolean cache = false;
for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
addDependentResources(pin, inc);
cache = addDependentResources(pin, inc) || cache;
}
for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
addDependentResources(pin, inc);
cache = addDependentResources(pin, inc) || cache;
}
return cache;
}
private void addDependentResources(Parameters pin, ConceptSetComponent inc) {
private boolean addDependentResources(Parameters pin, ConceptSetComponent inc) {
boolean cache = false;
for (CanonicalType c : inc.getValueSet()) {
ValueSet vs = fetchResource(ValueSet.class, c.getValue());
if (vs != null) {
if (isTxCaching && cacheId == null || !cached.contains(vs.getVUrl())) {
pin.addParameter().setName("tx-resource").setResource(vs);
cached.add(vs.getVUrl());
cache = true;
}
addDependentResources(pin, vs);
}
@ -1000,9 +1036,11 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
if (isTxCaching && cacheId == null || !cached.contains(cs.getVUrl())) {
pin.addParameter().setName("tx-resource").setResource(cs);
cached.add(cs.getVUrl());
cache = true;
}
// todo: supplements
}
}
return cache;
}
public ValidationResult processValidationResult(Parameters pOut) {
@ -1932,5 +1970,22 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
guides.size() + capstmts.size() + searchParameters.size() + questionnaires.size() + operations.size() + plans.size() + systems.size();
}
public Set<String> getCodeSystemsUsed() {
return codeSystemsUsed ;
}
public String getSpecUrl() {
String v = getVersion();
switch (VersionUtilities.getMajMin(v)) {
case "1.0" : return "http://hl7.org/fhir/DSTU1";
case "1.4" : return "http://hl7.org/fhir/DSTU2";
case "3.0" : return "http://hl7.org/fhir/STU3";
case "4.0" : return "http://hl7.org/fhir/R4";
case "4.5" : return "http://build.fhir.org";
case "5.0" : return "http://build.fhir.org";
default:
return "http://hl7.org/fhir";
}
}
}

View File

@ -222,7 +222,12 @@ public interface IWorkerContext {
* Get the versions of the definitions loaded in context
* @return
*/
public String getVersion();
public String getVersion();
/**
* return the link to the base of the specification for the loaded version e.g. http://hl7.org/fhir/R4
*/
public String getSpecUrl();
// get the UCUM service (might not be available)
public UcumService getUcumService();
@ -710,7 +715,7 @@ public interface IWorkerContext {
public ILoggingService getLogger();
public boolean isNoTerminologyServer();
public Set<String> getCodeSystemsUsed();
public TranslationServices translator();
public List<StructureMap> listTransforms();
public StructureMap getTransform(String url);

View File

@ -76,6 +76,7 @@ import org.hl7.fhir.r5.model.StructureMap.StructureMapModelMode;
import org.hl7.fhir.r5.model.StructureMap.StructureMapStructureComponent;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.IResourceValidator;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.CSFileInputStream;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.TimeTracker;
@ -138,7 +139,8 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
}
public interface IValidatorFactory {
IResourceValidator makeValidator(IWorkerContext ctxts) throws FHIRException;
IResourceValidator makeValidator(IWorkerContext ctxt) throws FHIRException;
IResourceValidator makeValidator(IWorkerContext ctxts, XVerExtensionManager xverManager) throws FHIRException;
}
private Questionnaire questionnaire;
@ -149,6 +151,7 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
private boolean progress;
private List<String> loadedPackages = new ArrayList<String>();
private boolean canNoTS;
private XVerExtensionManager xverManager;
public SimpleWorkerContext() throws FileNotFoundException, IOException, FHIRException {
super();
@ -243,19 +246,15 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
}
public static SimpleWorkerContext fromClassPath(String name) throws IOException, FHIRException {
InputStream s = SimpleWorkerContext.class.getResourceAsStream("/"+name);
SimpleWorkerContext res = new SimpleWorkerContext();
res.loadFromStream(s, null);
return res;
return fromClassPath(name, false);
}
public static SimpleWorkerContext fromClassPath(String name, boolean allowDuplicates) throws IOException, FHIRException {
InputStream s = SimpleWorkerContext.class.getResourceAsStream("/" + name);
SimpleWorkerContext res = new SimpleWorkerContext();
res.setAllowLoadingDuplicates(allowDuplicates);
res.loadFromStream(s, null);
return res;
}
// public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source) throws IOException, FHIRException {
// SimpleWorkerContext res = new SimpleWorkerContext();
// for (String name : source.keySet()) {
// res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), null, null);
// }
// return res;
// }
public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source, IContextResourceLoader loader, PackageVersion pi) throws FileNotFoundException, IOException, FHIRException {
SimpleWorkerContext res = new SimpleWorkerContext();
@ -539,7 +538,7 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
public IResourceValidator newValidator() throws FHIRException {
if (validatorFactory == null)
throw new Error(formatMessage(I18nConstants.NO_VALIDATOR_CONFIGURED));
return validatorFactory.makeValidator(this);
return validatorFactory.makeValidator(this, xverManager);
}
@ -770,6 +769,10 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
ProfileUtilities pu = new ProfileUtilities(this, msgs, this);
pu.setAutoFixSliceNames(true);
pu.setThrowException(false);
if (xverManager == null) {
xverManager = new XVerExtensionManager(this);
}
pu.setXver(xverManager);
if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
pu.sortDifferential(sd, p, p.getUrl(), errors, true);
}
@ -812,6 +815,10 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
return loadedPackages.contains(id+"#"+ver);
}
public boolean hasPackage(String idAndver) {
return loadedPackages.contains(idAndver);
}
public void setClock(TimeTracker tt) {
clock = tt;
}
@ -823,6 +830,13 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
public void setCanNoTS(boolean canNoTS) {
this.canNoTS = canNoTS;
}
public XVerExtensionManager getXVer() {
if (xverManager == null) {
xverManager = new XVerExtensionManager(this);
}
return xverManager;
}
}
}

View File

@ -109,17 +109,31 @@ public class Property {
return definition.getPath();
ElementDefinition ed = definition;
if (definition.hasContentReference()) {
if (!definition.getContentReference().startsWith("#"))
throw new Error("not handled yet");
String url = null;
String path = definition.getContentReference();
if (!path.startsWith("#")) {
if (path.contains("#")) {
url = path.substring(0, path.indexOf("#"));
path = path.substring(path.indexOf("#")+1);
} else {
throw new Error("Illegal content reference '"+path+"'");
}
} else {
path = path.substring(1);
}
StructureDefinition sd = (url == null || url.equals(structure.getUrl())) ? structure : context.fetchResource(StructureDefinition.class, url, structure);
if (sd == null) {
throw new Error("Unknown Type in content reference '"+path+"'");
}
boolean found = false;
for (ElementDefinition d : structure.getSnapshot().getElement()) {
if (d.hasId() && d.getId().equals(definition.getContentReference().substring(1))) {
for (ElementDefinition d : sd.getSnapshot().getElement()) {
if (d.hasId() && d.getId().equals(path)) {
found = true;
ed = d;
}
}
if (!found)
throw new Error("Unable to resolve "+definition.getContentReference()+" at "+definition.getPath()+" on "+structure.getUrl());
throw new Error("Unable to resolve "+definition.getContentReference()+" at "+definition.getPath()+" on "+sd.getUrl());
}
if (ed.getType().size() == 0)
return null;

View File

@ -899,16 +899,12 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
lowRight = lowRight - (14 * DateUtils.MILLIS_PER_HOUR);
highRight = highRight + (14 * DateUtils.MILLIS_PER_HOUR);
}
System.out.print("["+((lowLeft / 1000) - 130000000)+"-"+((highLeft / 1000) - 130000000)+"] vs ["+((lowRight / 1000) - 130000000)+"-"+((highRight / 1000) - 130000000)+"] = ");
if (highRight < lowLeft) {
System.out.println("false");
return false;
}
if (highLeft < lowRight) {
System.out.println("false");
return false;
}
System.out.println("true");
return true;
}
@ -1000,6 +996,14 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
return def;
}
if (left.getSecond() < right.getSecond()) {
return -1;
} else if (left.getSecond() > right.getSecond()) {
return 1;
} else if (left.getPrecision() == TemporalPrecisionEnum.SECOND && right.getPrecision() == TemporalPrecisionEnum.SECOND) {
return 0;
}
if (left.getSecondsMilli() < right.getSecondsMilli()) {
return -1;
} else if (left.getSecondsMilli() > right.getSecondsMilli()) {

View File

@ -63,7 +63,7 @@ public class BundleRenderer extends ResourceRenderer {
List<BaseWrapper> entries = b.children("entry");
if ("document".equals(b.get("type").primitiveValue())) {
if (entries.isEmpty() || (entries.get(0).has("resource") && "Composition".equals(entries.get(0).get("resource").fhirType())))
throw new FHIRException("Invalid document - first entry is not a Composition");
throw new FHIRException("Invalid document '"+b.getId()+"' - first entry is not a Composition ('"+entries.get(0).get("resource").fhirType()+"')");
return renderDocument(x, b, entries);
} else if ("collection".equals(b.get("type").primitiveValue()) && allEntriesAreHistoryProvenance(entries)) {
// nothing
@ -399,4 +399,16 @@ public class BundleRenderer extends ResourceRenderer {
return "??";
}
public boolean canRender(Bundle b) {
for (BundleEntryComponent be : b.getEntry()) {
if (be.hasResource()) {
ResourceRenderer rr = RendererFactory.factory(be.getResource(), context);
if (!rr.canRender(be.getResource())) {
return false;
}
}
}
return true;
}
}

View File

@ -41,7 +41,7 @@ public class CapabilityStatementRenderer extends ResourceRenderer {
CapabilityStatementRestComponent rest = conf.getRest().get(0);
XhtmlNode t = x.table(null);
addTableRow(t, "Mode", rest.getMode().toString());
addTableRow(t, "Description", rest.getDocumentation());
addMarkdown(addTableRow(t, "Description"), rest.getDocumentation());
addTableRow(t, "Transaction", showOp(rest, SystemRestfulInteraction.TRANSACTION));
addTableRow(t, "System History", showOp(rest, SystemRestfulInteraction.HISTORYSYSTEM));
@ -143,7 +143,12 @@ public class CapabilityStatementRenderer extends ResourceRenderer {
return "";
}
private XhtmlNode addTableRow(XhtmlNode t, String name) {
XhtmlNode tr = t.tr();
tr.td().addText(name);
return tr.td();
}
private void addTableRow(XhtmlNode t, String name, String value) {
XhtmlNode tr = t.tr();
tr.td().addText(name);

View File

@ -57,6 +57,7 @@ import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.MarkDownProcessor;
import org.hl7.fhir.utilities.MarkDownProcessor.Dialect;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
@ -219,11 +220,18 @@ public class DataRenderer extends Renderer {
private boolean isCanonical(String path) {
if (!path.endsWith(".url"))
return false;
StructureDefinition sd = getContext().getWorker().fetchTypeDefinition(path.substring(0, path.length()-4));
String t = path.substring(0, path.length()-4);
StructureDefinition sd = getContext().getWorker().fetchTypeDefinition(t);
if (sd == null)
return false;
if (Utilities.existsInList(path.substring(0, path.length()-4), "CapabilityStatement", "StructureDefinition", "ImplementationGuide", "SearchParameter", "MessageDefinition", "OperationDefinition", "CompartmentDefinition", "StructureMap", "GraphDefinition",
"ExampleScenario", "CodeSystem", "ValueSet", "ConceptMap", "NamingSystem", "TerminologyCapabilities"))
if (Utilities.existsInList(t, VersionUtilities.getCanonicalResourceNames(getContext().getWorker().getVersion()))) {
return true;
}
if (Utilities.existsInList(t,
"ActivityDefinition", "CapabilityStatement", "CapabilityStatement2", "ChargeItemDefinition", "Citation", "CodeSystem",
"CompartmentDefinition", "ConceptMap", "ConditionDefinition", "EventDefinition", "Evidence", "EvidenceReport", "EvidenceVariable",
"ExampleScenario", "GraphDefinition", "ImplementationGuide", "Library", "Measure", "MessageDefinition", "NamingSystem", "PlanDefinition"
))
return true;
return sd.getBaseDefinitionElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-codegen-super");
}
@ -381,26 +389,20 @@ public class DataRenderer extends Renderer {
}
protected void renderUri(XhtmlNode x, UriType uri, String path, String id) {
String url = uri.getValue();
if (isCanonical(path)) {
CanonicalResource mr = getContext().getWorker().fetchResource(null, url);
if (mr != null) {
if (path.startsWith(mr.fhirType()+".") && mr.getId().equals(id)) {
url = null; // don't link to self whatever
} else if (mr.hasUserData("path"))
url = mr.getUserString("path");
} else if (!getContext().isCanonicalUrlsAsLinks())
url = null;
}
if (url == null) {
x.b().tx(uri.getValue());
} else if (uri.getValue().startsWith("mailto:")) {
x.ah(uri.getValue()).addText(uri.getValue().substring(7));
x.code().tx(uri.getValue());
} else {
if (uri.getValue().contains("|")) {
x.ah(uri.getValue().substring(0, uri.getValue().indexOf("|"))).addText(uri.getValue());
String url = uri.getValue();
if (url == null) {
x.b().tx(uri.getValue());
} else if (uri.getValue().startsWith("mailto:")) {
x.ah(uri.getValue()).addText(uri.getValue().substring(7));
} else {
x.ah(uri.getValue()).addText(uri.getValue());
if (uri.getValue().contains("|")) {
x.ah(uri.getValue().substring(0, uri.getValue().indexOf("|"))).addText(uri.getValue());
} else {
x.ah(uri.getValue()).addText(uri.getValue());
}
}
}
}

View File

@ -939,5 +939,8 @@ public class ProfileDrivenRenderer extends ResourceRenderer {
return path.substring(path.lastIndexOf(".")+1);
}
public boolean canRender(Resource resource) {
return context.getWorker().getResourceNames().contains(resource.fhirType());
}
}

View File

@ -366,4 +366,8 @@ public abstract class ResourceRenderer extends DataRenderer {
return fullUrl.replace(":", "-");
}
public boolean canRender(Resource resource) {
return true;
}
}

View File

@ -35,6 +35,7 @@ import java.util.Map;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
@ -54,4 +55,5 @@ public interface TerminologyClient {
public CapabilityStatement getCapabilitiesStatementQuick() throws FHIRException;
public Parameters lookupCode(Map<String, String> params) throws FHIRException;
public Bundle validateBatch(Bundle batch);
public CanonicalResource read(String type, String id);
}

View File

@ -34,6 +34,7 @@ package org.hl7.fhir.r5.utils;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Locale;
@ -42,6 +43,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.utils.IResourceValidator.BundleValidationRule;
import org.hl7.fhir.utilities.Utilities;
@ -102,6 +104,7 @@ public interface IResourceValidator {
void recordProfileUsage(StructureDefinition profile, Object appContext, Element element);
}
public interface IValidatorResourceFetcher {
Element fetch(Object appContext, String url) throws FHIRFormatError, DefinitionException, FHIRException, IOException;
@ -111,6 +114,29 @@ public interface IResourceValidator {
byte[] fetchRaw(String url) throws MalformedURLException, IOException; // for attachment checking
void setLocale(Locale locale);
/**
* this is used when the validator encounters a reference to a structure definition, value set or code system at some random URL reference
* while validating.
*
* Added in v5.2.2. return null to leave functionality as it was before then.
*
* @param primitiveValue
* @return an R5 version of the resource
* @throws URISyntaxException
*/
CanonicalResource fetchCanonicalResource(String url) throws URISyntaxException;
/**
* Whether to try calling fetchCanonicalResource for this reference (not whether it will succeed - just throw an exception from fetchCanonicalResource if it doesn't resolve. This is a policy thing.
*
* Added in v5.2.2. return false to leave functionality as it was before then.
*
* @param url
* @return
*/
boolean fetchesCanonicalResource(String url);
}
public enum BestPracticeWarningLevel {

View File

@ -84,6 +84,7 @@ public class XVerExtensionManager {
StructureDefinition sd = new StructureDefinition();
sd.setUserData(XVER_EXT_MARKER, "true");
sd.setUserData("path", "http://hl7.org/fhir/versions.html#extensions");
sd.setUrl(url);
sd.setVersion(context.getVersion());
sd.setFhirVersion(FHIRVersion.fromCode(context.getVersion()));
@ -135,7 +136,11 @@ public class XVerExtensionManager {
if (hasTargets(tr.getCode()) ) {
s = s.substring(t.length()+1);
for (String p : s.substring(0, s.length()-1).split("\\|")) {
tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+p);
if ("Any".equals(p)) {
tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource");
} else {
tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+p);
}
}
}
} else {

View File

@ -1,684 +0,0 @@
package org.hl7.fhir.r5.utils.client;
/*
Copyright (c) 2011+, HL7, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of HL7 nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.hl7.fhir.r5.formats.IParser;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.formats.XmlParser;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.OperationOutcome;
import org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.ResourceType;
import org.hl7.fhir.r5.utils.ResourceUtilities;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
/**
* Helper class handling lower level HTTP transport concerns.
* TODO Document methods.
* @author Claude Nanjo
*/
public class ClientUtils {
public static final String DEFAULT_CHARSET = "UTF-8";
public static final String HEADER_LOCATION = "location";
private static boolean debugging = false;
public static final int TIMEOUT_SOCKET = 5000;
public static final int TIMEOUT_CONNECT = 1000;
private HttpHost proxy;
private int timeout = TIMEOUT_SOCKET;
private String username;
private String password;
private ToolingClientLogger logger;
private int retryCount;
private HttpClient httpclient;
public HttpHost getProxy() {
return proxy;
}
public void setProxy(HttpHost proxy) {
this.proxy = proxy;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat, String message, int timeout) {
HttpOptions options = new HttpOptions(optionsUri);
return issueResourceRequest(resourceFormat, options, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat, String message, int timeout) {
HttpGet httpget = new HttpGet(resourceUri);
return issueResourceRequest(resourceFormat, httpget, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers, String message, int timeout) {
HttpPut httpPut = new HttpPut(resourceUri);
return issueResourceRequest(resourceFormat, httpPut, payload, headers, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) {
HttpPut httpPut = new HttpPut(resourceUri);
return issueResourceRequest(resourceFormat, httpPut, payload, null, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers, String message, int timeout) {
HttpPost httpPost = new HttpPost(resourceUri);
return issueResourceRequest(resourceFormat, httpPost, payload, headers, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) {
return issuePostRequest(resourceUri, payload, resourceFormat, null, message, timeout);
}
public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) {
HttpGet httpget = new HttpGet(resourceUri);
configureFhirRequest(httpget, resourceFormat);
HttpResponse response = sendRequest(httpget);
return unmarshalReference(response, resourceFormat);
}
private void setAuth(HttpRequest httpget) {
if (password != null) {
try {
byte[] b = Base64.encodeBase64((username+":"+password).getBytes("ASCII"));
String b64 = new String(b, StandardCharsets.US_ASCII);
httpget.setHeader("Authorization", "Basic " + b64);
} catch (UnsupportedEncodingException e) {
}
}
}
public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) {
HttpPost httpPost = new HttpPost(resourceUri);
configureFhirRequest(httpPost, resourceFormat);
HttpResponse response = sendPayload(httpPost, payload, proxy, message, timeout);
return unmarshalFeed(response, resourceFormat);
}
public boolean issueDeleteRequest(URI resourceUri) {
HttpDelete deleteRequest = new HttpDelete(resourceUri);
HttpResponse response = sendRequest(deleteRequest);
int responseStatusCode = response.getStatusLine().getStatusCode();
boolean deletionSuccessful = false;
if(responseStatusCode == 204) {
deletionSuccessful = true;
}
return deletionSuccessful;
}
/***********************************************************
* Request/Response Helper methods
***********************************************************/
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, String message, int timeout) {
return issueResourceRequest(resourceFormat, request, null, message, timeout);
}
/**
* @param resourceFormat
* @param options
* @return
*/
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, String message, int timeout) {
return issueResourceRequest(resourceFormat, request, payload, null, message, timeout);
}
/**
* @param resourceFormat
* @param options
* @return
*/
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List<Header> headers, String message, int timeout) {
configureFhirRequest(request, resourceFormat, headers);
HttpResponse response = null;
if(request instanceof HttpEntityEnclosingRequest && payload != null) {
response = sendPayload((HttpEntityEnclosingRequestBase)request, payload, proxy, message, timeout);
} else if (request instanceof HttpEntityEnclosingRequest && payload == null){
throw new EFhirClientException("PUT and POST requests require a non-null payload");
} else {
response = sendRequest(request);
}
T resource = unmarshalReference(response, resourceFormat);
return new ResourceRequest<T>(resource, response.getStatusLine().getStatusCode(), getLocationHeader(response));
}
/**
* Method adds required request headers.
* TODO handle JSON request as well.
*
* @param request
*/
protected void configureFhirRequest(HttpRequest request, String format) {
configureFhirRequest(request, format, null);
}
/**
* Method adds required request headers.
* TODO handle JSON request as well.
*
* @param request
*/
protected void configureFhirRequest(HttpRequest request, String format, List<Header> headers) {
request.addHeader("User-Agent", "hapi-fhir-tooling-client");
if (format != null) {
request.addHeader("Accept",format);
request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET);
}
request.addHeader("Accept-Charset", DEFAULT_CHARSET);
if(headers != null) {
for(Header header : headers) {
request.addHeader(header);
}
}
setAuth(request);
}
/**
* Method posts request payload
*
* @param request
* @param payload
* @return
*/
@SuppressWarnings({ "resource", "deprecation" })
protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy, String message, int timeout) {
HttpResponse response = null;
boolean ok = false;
long t = System.currentTimeMillis();
int tryCount = 0;
while (!ok) {
try {
tryCount++;
if (httpclient == null) {
makeClient(proxy);
}
HttpParams params = httpclient.getParams();
HttpConnectionParams.setSoTimeout(params, timeout < 1 ? this.timeout : timeout * 1000);
request.setEntity(new ByteArrayEntity(payload));
log(request);
response = httpclient.execute(request);
ok = true;
} catch(IOException ioe) {
System.out.println(ioe.getMessage()+" ("+(System.currentTimeMillis()-t)+"ms / "+Utilities.describeSize(payload.length)+" for "+message+")");
if (tryCount <= retryCount || (tryCount < 3 && ioe instanceof org.apache.http.conn.ConnectTimeoutException)) {
ok = false;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
} else {
if (tryCount > 4) {
System.out.println("Giving up: "+ioe.getMessage()+" (R5 / "+(System.currentTimeMillis()-t)+"ms / "+Utilities.describeSize(payload.length)+" for "+message+")");
}
throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe);
}
}
}
return response;
}
@SuppressWarnings("deprecation")
public void makeClient(HttpHost proxy) {
httpclient = new DefaultHttpClient();
HttpParams params = httpclient.getParams();
HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_CONNECT);
HttpConnectionParams.setSoTimeout(params, timeout);
HttpConnectionParams.setSoKeepalive(params, true);
if(proxy != null) {
httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
}
}
/**
*
* @param request
* @param payload
* @return
*/
protected HttpResponse sendRequest(HttpUriRequest request) {
HttpResponse response = null;
try {
if (httpclient == null) {
makeClient(proxy);
}
response = httpclient.execute(request);
} catch(IOException ioe) {
if (ClientUtils.debugging ) {
ioe.printStackTrace();
}
throw new EFhirClientException("Error sending Http Request: "+ioe.getMessage(), ioe);
}
return response;
}
/**
* Unmarshals a resource from the response stream.
*
* @param response
* @return
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(HttpResponse response, String format) {
T resource = null;
OperationOutcome error = null;
byte[] cnt = log(response);
if (cnt != null) {
try {
resource = (T)getParser(format).parse(cnt);
if (resource instanceof OperationOutcome && hasError((OperationOutcome)resource)) {
error = (OperationOutcome) resource;
}
} catch(IOException ioe) {
throw new EFhirClientException("Error reading Http Response: "+ioe.getMessage(), ioe);
} catch(Exception e) {
throw new EFhirClientException("Error parsing response message: "+e.getMessage(), e);
}
}
if(error != null) {
throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error);
}
return resource;
}
/**
* Unmarshals Bundle from response stream.
*
* @param response
* @return
*/
protected Bundle unmarshalFeed(HttpResponse response, String format) {
Bundle feed = null;
byte[] cnt = log(response);
String contentType = response.getHeaders("Content-Type")[0].getValue();
OperationOutcome error = null;
try {
if (cnt != null) {
if(contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) {
Resource rf = getParser(format).parse(cnt);
if (rf instanceof Bundle)
feed = (Bundle) rf;
else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) {
error = (OperationOutcome) rf;
} else {
throw new EFhirClientException("Error reading server response: a resource was returned instead");
}
}
}
} catch(IOException ioe) {
throw new EFhirClientException("Error reading Http Response", ioe);
} catch(Exception e) {
throw new EFhirClientException("Error parsing response message", e);
}
if(error != null) {
throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error);
}
return feed;
}
private boolean hasError(OperationOutcome oo) {
for (OperationOutcomeIssueComponent t : oo.getIssue())
if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL)
return true;
return false;
}
protected String getLocationHeader(HttpResponse response) {
String location = null;
if(response.getHeaders("location").length > 0) {//TODO Distinguish between both cases if necessary
location = response.getHeaders("location")[0].getValue();
} else if(response.getHeaders("content-location").length > 0) {
location = response.getHeaders("content-location")[0].getValue();
}
return location;
}
/*****************************************************************
* Client connection methods
* ***************************************************************/
public HttpURLConnection buildConnection(URI baseServiceUri, String tail) {
try {
HttpURLConnection client = (HttpURLConnection) baseServiceUri.resolve(tail).toURL().openConnection();
return client;
} catch(MalformedURLException mue) {
throw new EFhirClientException("Invalid Service URL", mue);
} catch(IOException ioe) {
throw new EFhirClientException("Unable to establish connection to server: " + baseServiceUri.toString() + tail, ioe);
}
}
public HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) {
return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id));
}
/******************************************************************
* Other general helper methods
* ****************************************************************/
public <T extends Resource> byte[] getResourceAsByteArray(T resource, boolean pretty, boolean isJson) {
ByteArrayOutputStream baos = null;
byte[] byteArray = null;
try {
baos = new ByteArrayOutputStream();
IParser parser = null;
if(isJson) {
parser = new JsonParser();
} else {
parser = new XmlParser();
}
parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL);
parser.compose(baos, resource);
baos.close();
byteArray = baos.toByteArray();
baos.close();
} catch (Exception e) {
try{
baos.close();
}catch(Exception ex) {
throw new EFhirClientException("Error closing output stream", ex);
}
throw new EFhirClientException("Error converting output stream to byte array", e);
}
return byteArray;
}
public byte[] getFeedAsByteArray(Bundle feed, boolean pretty, boolean isJson) {
ByteArrayOutputStream baos = null;
byte[] byteArray = null;
try {
baos = new ByteArrayOutputStream();
IParser parser = null;
if(isJson) {
parser = new JsonParser();
} else {
parser = new XmlParser();
}
parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL);
parser.compose(baos, feed);
baos.close();
byteArray = baos.toByteArray();
baos.close();
} catch (Exception e) {
try{
baos.close();
}catch(Exception ex) {
throw new EFhirClientException("Error closing output stream", ex);
}
throw new EFhirClientException("Error converting output stream to byte array", e);
}
return byteArray;
}
public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) {
String dateTime = null;
try {
dateTime = serverConnection.getHeaderField("Last-Modified");
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", new Locale("en", "US"));
Date lastModifiedTimestamp = format.parse(dateTime);
Calendar calendar=Calendar.getInstance();
calendar.setTime(lastModifiedTimestamp);
return calendar;
} catch(ParseException pe) {
throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe);
}
}
protected IParser getParser(String format) {
if(StringUtils.isBlank(format)) {
format = ResourceFormat.RESOURCE_XML.getHeader();
}
if(format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) {
return new JsonParser();
} else if(format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) {
return new XmlParser();
} else {
throw new EFhirClientException("Invalid format: " + format);
}
}
public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName, Resource resource, String resourceFormat) throws IOException {
HttpPost httppost = new HttpPost(resourceUri);
String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy";
httppost.addHeader("Content-Type", "multipart/form-data; boundary="+boundary);
httppost.addHeader("Accept", resourceFormat);
configureFhirRequest(httppost, null);
HttpResponse response = sendPayload(httppost, encodeFormSubmission(parameters, resourceName, resource, boundary));
return unmarshalFeed(response, resourceFormat);
}
private byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource, String boundary) throws IOException {
ByteArrayOutputStream b = new ByteArrayOutputStream();
OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8");
for (String name : parameters.keySet()) {
w.write("--");
w.write(boundary);
w.write("\r\nContent-Disposition: form-data; name=\""+name+"\"\r\n\r\n");
w.write(parameters.get(name)+"\r\n");
}
w.write("--");
w.write(boundary);
w.write("\r\nContent-Disposition: form-data; name=\""+resourceName+"\"\r\n\r\n");
w.close();
JsonParser json = new JsonParser();
json.setOutputStyle(OutputStyle.NORMAL);
json.compose(b, resource);
b.close();
w = new OutputStreamWriter(b, "UTF-8");
w.write("\r\n--");
w.write(boundary);
w.write("--");
w.close();
return b.toByteArray();
}
/**
* Method posts request payload
*
* @param request
* @param payload
* @return
*/
protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) {
HttpResponse response = null;
try {
log(request);
if (httpclient == null) {
makeClient(proxy);
}
request.setEntity(new ByteArrayEntity(payload));
response = httpclient.execute(request);
log(response);
} catch(IOException ioe) {
throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe);
}
return response;
}
private void log(HttpUriRequest request) {
if (logger != null) {
List<String> headers = new ArrayList<>();
for (Header h : request.getAllHeaders()) {
headers.add(h.toString());
}
logger.logRequest(request.getMethod(), request.getURI().toString(), headers, null);
}
}
private void log(HttpEntityEnclosingRequestBase request) {
if (logger != null) {
List<String> headers = new ArrayList<>();
for (Header h : request.getAllHeaders()) {
headers.add(h.toString());
}
byte[] cnt = null;
InputStream s;
try {
s = request.getEntity().getContent();
cnt = IOUtils.toByteArray(s);
s.close();
} catch (Exception e) {
}
logger.logRequest(request.getMethod(), request.getURI().toString(), headers, cnt);
}
}
private byte[] log(HttpResponse response) {
byte[] cnt = null;
try {
InputStream s = response.getEntity().getContent();
cnt = IOUtils.toByteArray(s);
s.close();
} catch (Exception e) {
}
if (logger != null) {
List<String> headers = new ArrayList<>();
for (Header h : response.getAllHeaders()) {
headers.add(h.toString());
}
logger.logResponse(response.getStatusLine().toString(), headers, cnt);
}
return cnt;
}
public ToolingClientLogger getLogger() {
return logger;
}
public void setLogger(ToolingClientLogger logger) {
this.logger = logger;
}
/**
* Used for debugging
*
* @param instream
* @return
*/
protected String writeInputStreamAsString(InputStream instream) {
String value = null;
try {
value = IOUtils.toString(instream, "UTF-8");
System.out.println(value);
} catch(IOException ioe) {
//Do nothing
}
return value;
}
public int getRetryCount() {
return retryCount;
}
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
}

View File

@ -99,7 +99,6 @@ public class EFhirClientException extends RuntimeException {
* A default message of "One or more server side errors have occurred during this operation. Refer to e.getServerErrors() for additional details."
* will be returned to users.
*
* @param message
* @param serverError
*/
public EFhirClientException(OperationOutcome serverError) {

View File

@ -228,9 +228,6 @@ public class ResourceAddress {
/**
* For now, assume this type of location header structure.
* Generalize later: http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1
*
* @param serviceBase
* @param locationHeader
*/
public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);

View File

@ -1,109 +0,0 @@
package org.hl7.fhir.r5.utils.client;
/*
Copyright (c) 2011+, HL7, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of HL7 nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.r5.model.Resource;
public class ResourceRequest<T extends Resource> {
private T payload;
private int httpStatus = -1;
private String location;
private List<Integer> successfulStatuses = new ArrayList<Integer>();
private List<Integer> errorStatuses = new ArrayList<Integer>();
public ResourceRequest(T payload, int httpStatus, List<Integer> successfulStatuses, List<Integer> errorStatuses, String location) {
this.payload = payload;
this.httpStatus = httpStatus;
if(successfulStatuses != null) {
this.successfulStatuses.addAll(successfulStatuses);
}
if(errorStatuses != null) {
this.errorStatuses.addAll(errorStatuses);
}
this.location = location;
}
public ResourceRequest(T payload, int httpStatus, String location) {
this.payload = payload;
this.httpStatus = httpStatus;
this.location = location;
}
public ResourceRequest(T payload, int httpStatus, int successfulStatus, String location) {
this.payload = payload;
this.httpStatus = httpStatus;
this.successfulStatuses.add(successfulStatus);
this.location = location;
}
public int getHttpStatus() {
return httpStatus;
}
public T getPayload() {
return payload;
}
public T getReference() {
T payloadResource = null;
if(payload != null) {
payloadResource = payload;
}
return payloadResource;
}
public boolean isSuccessfulRequest() {
return successfulStatuses.contains(httpStatus) && !errorStatuses.contains(httpStatus) && httpStatus > 0;
}
public boolean isUnsuccessfulRequest() {
return !isSuccessfulRequest();
}
public void addSuccessStatus(int status) {
this.successfulStatuses.add(status);
}
public void addErrorStatus(int status) {
this.errorStatuses.add(status);
}
public String getLocation() {
return location;
}
}

View File

@ -0,0 +1,69 @@
package org.hl7.fhir.r5.utils.client.network;
import org.hl7.fhir.r5.formats.IParser;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.formats.XmlParser;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.utils.client.EFhirClientException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class ByteUtils {
public static <T extends Resource> byte[] resourceToByteArray(T resource, boolean pretty, boolean isJson) {
ByteArrayOutputStream baos = null;
byte[] byteArray = null;
try {
baos = new ByteArrayOutputStream();
IParser parser = null;
if (isJson) {
parser = new JsonParser();
} else {
parser = new XmlParser();
}
parser.setOutputStyle(pretty ? IParser.OutputStyle.PRETTY : IParser.OutputStyle.NORMAL);
parser.compose(baos, resource);
baos.close();
byteArray = baos.toByteArray();
baos.close();
} catch (Exception e) {
try {
baos.close();
} catch (Exception ex) {
throw new EFhirClientException("Error closing output stream", ex);
}
throw new EFhirClientException("Error converting output stream to byte array", e);
}
return byteArray;
}
public static byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource, String boundary) throws IOException {
ByteArrayOutputStream b = new ByteArrayOutputStream();
OutputStreamWriter w = new OutputStreamWriter(b, StandardCharsets.UTF_8);
for (String name : parameters.keySet()) {
w.write("--");
w.write(boundary);
w.write("\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
w.write(parameters.get(name) + "\r\n");
}
w.write("--");
w.write(boundary);
w.write("\r\nContent-Disposition: form-data; name=\"" + resourceName + "\"\r\n\r\n");
w.close();
JsonParser json = new JsonParser();
json.setOutputStyle(IParser.OutputStyle.NORMAL);
json.compose(b, resource);
b.close();
w = new OutputStreamWriter(b, StandardCharsets.UTF_8);
w.write("\r\n--");
w.write(boundary);
w.write("--");
w.close();
return b.toByteArray();
}
}

View File

@ -0,0 +1,234 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class Client {
public static final String DEFAULT_CHARSET = "UTF-8";
private static final long DEFAULT_TIMEOUT = 5000;
private String username;
private String password;
private ToolingClientLogger logger;
private int retryCount;
private long timeout = DEFAULT_TIMEOUT;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public ToolingClientLogger getLogger() {
return logger;
}
public void setLogger(ToolingClientLogger logger) {
this.logger = logger;
}
public int getRetryCount() {
return retryCount;
}
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri,
String resourceFormat,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.method("OPTIONS", null)
.url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, null, message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeFhirRequest(request, resourceFormat, null, message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, null, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("PUT requests require a non-null payload");
RequestBody body = RequestBody.create(payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.put(body);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePostRequest(resourceUri, payload, resourceFormat, null, message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public boolean issueDeleteRequest(URI resourceUri) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.delete();
return executeFhirRequest(request, null, null, null, retryCount, timeout).isSuccessfulRequest();
}
public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeBundleRequest(request, resourceFormat, null, null, retryCount, timeout);
}
public Bundle issuePostFeedRequest(URI resourceUri,
Map<String, String> parameters,
String resourceName,
Resource resource,
String resourceFormat) throws IOException {
String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy";
byte[] payload = ByteUtils.encodeFormSubmission(parameters, resourceName, resource, boundary);
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeBundleRequest(request, resourceFormat, null, null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
int timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeBundleRequest(request, resourceFormat, null, message, retryCount, timeout);
}
protected <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
return new FhirRequestBuilder(request)
.withLogger(logger)
.withResourceFormat(resourceFormat)
.withRetryCount(retryCount)
.withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS)
.executeAsBatch();
}
protected <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
return new FhirRequestBuilder(request)
.withLogger(logger)
.withResourceFormat(resourceFormat)
.withRetryCount(retryCount)
.withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS)
.execute();
}
/**
* @deprecated It does not appear as though this method is actually being used. Will be removed in a future release
* unless a case is made to keep it.
*/
@Deprecated
public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) {
String dateTime = null;
try {
dateTime = serverConnection.getHeaderField("Last-Modified");
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", new Locale("en", "US"));
Date lastModifiedTimestamp = format.parse(dateTime);
Calendar calendar = Calendar.getInstance();
calendar.setTime(lastModifiedTimestamp);
return calendar;
} catch (ParseException pe) {
throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe);
}
}
}

View File

@ -0,0 +1,328 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r5.formats.IParser;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.formats.XmlParser;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.OperationOutcome;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.utils.ResourceUtilities;
import org.hl7.fhir.r5.utils.client.EFhirClientException;
import org.hl7.fhir.r5.utils.client.ResourceFormat;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class FhirRequestBuilder {
protected static final String HTTP_PROXY_USER = "http.proxyUser";
protected static final String HTTP_PROXY_PASS = "http.proxyPassword";
protected static final String HEADER_PROXY_AUTH = "Proxy-Authorization";
protected static final String LOCATION_HEADER = "location";
protected static final String CONTENT_LOCATION_HEADER = "content-location";
protected static final String DEFAULT_CHARSET = "UTF-8";
/**
* The singleton instance of the HttpClient, used for all requests.
*/
private static OkHttpClient okHttpClient;
private final Request.Builder httpRequest;
private String resourceFormat = null;
private Headers headers = null;
private String message = null;
private int retryCount = 1;
/**
* The timeout quantity. Used in combination with {@link FhirRequestBuilder#timeoutUnit}.
*/
private long timeout = 5000;
/**
* Time unit for {@link FhirRequestBuilder#timeout}.
*/
private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
/**
* {@link ToolingClientLogger} for log output.
*/
private ToolingClientLogger logger = null;
public FhirRequestBuilder(Request.Builder httpRequest) {
this.httpRequest = httpRequest;
}
/**
* Adds necessary default headers, formatting headers, and any passed in {@link Headers} to the passed in
* {@link okhttp3.Request.Builder}
*
* @param request {@link okhttp3.Request.Builder} to add headers to.
* @param format Expected {@link Resource} format.
* @param headers Any additional {@link Headers} to add to the request.
*/
protected static void formatHeaders(Request.Builder request, String format, Headers headers) {
addDefaultHeaders(request);
if (format != null) addResourceFormatHeaders(request, format);
if (headers != null) addHeaders(request, headers);
}
/**
* Adds necessary headers for all REST requests.
* <li>User-Agent : hapi-fhir-tooling-client</li>
* <li>Accept-Charset : {@link FhirRequestBuilder#DEFAULT_CHARSET}</li>
*
* @param request {@link Request.Builder} to add default headers to.
*/
protected static void addDefaultHeaders(Request.Builder request) {
request.addHeader("User-Agent", "hapi-fhir-tooling-client");
request.addHeader("Accept-Charset", DEFAULT_CHARSET);
}
/**
* Adds necessary headers for the given resource format provided.
*
* @param request {@link Request.Builder} to add default headers to.
*/
protected static void addResourceFormatHeaders(Request.Builder request, String format) {
request.addHeader("Accept", format);
request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET);
}
/**
* Iterates through the passed in {@link Headers} and adds them to the provided {@link Request.Builder}.
*
* @param request {@link Request.Builder} to add headers to.
* @param headers {@link Headers} to add to request.
*/
protected static void addHeaders(Request.Builder request, Headers headers) {
headers.forEach(header -> request.addHeader(header.getFirst(), header.getSecond()));
}
/**
* Returns true if any of the {@link org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent} within the
* provided {@link OperationOutcome} have an {@link org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity} of
* {@link org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity#ERROR} or
* {@link org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity#FATAL}
*
* @param oo {@link OperationOutcome} to evaluate.
* @return {@link Boolean#TRUE} if an error exists.
*/
protected static boolean hasError(OperationOutcome oo) {
return (oo.getIssue().stream()
.anyMatch(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR
|| issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL));
}
/**
* Extracts the 'location' header from the passes in {@link Headers}. If no value for 'location' exists, the
* value for 'content-location' is returned. If neither header exists, we return null.
*
* @param headers {@link Headers} to evaluate
* @return {@link String} header value, or null if no location headers are set.
*/
protected static String getLocationHeader(Headers headers) {
Map<String, List<String>> headerMap = headers.toMultimap();
if (headerMap.containsKey(LOCATION_HEADER)) {
return headerMap.get(LOCATION_HEADER).get(0);
} else if (headerMap.containsKey(CONTENT_LOCATION_HEADER)) {
return headerMap.get(CONTENT_LOCATION_HEADER).get(0);
} else {
return null;
}
}
/**
* We only ever want to have one copy of the HttpClient kicking around at any given time. If we need to make changes
* to any configuration, such as proxy settings, timeout, caches, etc, we can do a per-call configuration through
* the {@link OkHttpClient#newBuilder()} method. That will return a builder that shares the same connection pool,
* dispatcher, and configuration with the original client.
* </p>
* The {@link OkHttpClient} uses the proxy auth properties set in the current system properties. The reason we don't
* set the proxy address and authentication explicitly, is due to the fact that this class is often used in conjunction
* with other http client tools which rely on the system.properties settings to determine proxy settings. It's easier
* to keep the method consistent across the board. ...for now.
*
* @return {@link OkHttpClient} instance
*/
protected OkHttpClient getHttpClient() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
Authenticator proxyAuthenticator = (route, response) -> {
String credential = Credentials.basic(System.getProperty(HTTP_PROXY_USER), System.getProperty(HTTP_PROXY_PASS));
return response.request().newBuilder()
.header(HEADER_PROXY_AUTH, credential)
.build();
};
return okHttpClient.newBuilder()
.addInterceptor(new RetryInterceptor(retryCount))
.connectTimeout(timeout, timeoutUnit)
.writeTimeout(timeout, timeoutUnit)
.readTimeout(timeout, timeoutUnit)
.proxyAuthenticator(proxyAuthenticator)
.build();
}
public FhirRequestBuilder withResourceFormat(String resourceFormat) {
this.resourceFormat = resourceFormat;
return this;
}
public FhirRequestBuilder withHeaders(Headers headers) {
this.headers = headers;
return this;
}
public FhirRequestBuilder withMessage(String message) {
this.message = message;
return this;
}
public FhirRequestBuilder withRetryCount(int retryCount) {
this.retryCount = retryCount;
return this;
}
public FhirRequestBuilder withLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) {
this.timeout = timeout;
this.timeoutUnit = unit;
return this;
}
protected Request buildRequest() {
return httpRequest.build();
}
public <T extends Resource> ResourceRequest<T> execute() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
T resource = unmarshalReference(response, resourceFormat);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
return unmarshalFeed(response, resourceFormat);
}
/**
* Unmarshalls a resource from the response stream.
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(Response response, String format) {
T resource = null;
OperationOutcome error = null;
if (response.body() != null) {
try {
byte[] body = response.body().bytes();
log(response.code(), response.headers(), body);
resource = (T) getParser(format).parse(body);
if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) {
error = (OperationOutcome) resource;
}
} catch (IOException ioe) {
throw new EFhirClientException("Error reading Http Response: " + ioe.getMessage(), ioe);
} catch (Exception e) {
throw new EFhirClientException("Error parsing response message: " + e.getMessage(), e);
}
}
if (error != null) {
throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error);
}
return resource;
}
/**
* Unmarshalls Bundle from response stream.
*/
protected Bundle unmarshalFeed(Response response, String format) {
Bundle feed = null;
OperationOutcome error = null;
try {
byte[] body = response.body().bytes();
log(response.code(), response.headers(), body);
String contentType = response.header("Content-Type");
if (body != null) {
if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) {
Resource rf = getParser(format).parse(body);
if (rf instanceof Bundle)
feed = (Bundle) rf;
else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) {
error = (OperationOutcome) rf;
} else {
throw new EFhirClientException("Error reading server response: a resource was returned instead");
}
}
}
} catch (IOException ioe) {
throw new EFhirClientException("Error reading Http Response", ioe);
} catch (Exception e) {
throw new EFhirClientException("Error parsing response message", e);
}
if (error != null) {
throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error);
}
return feed;
}
/**
* Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is
* provided...because reasons.
* <p>
* Currently supports only "json" and "xml" formats.
*
* @param format One of "json" or "xml".
* @return {@link JsonParser} or {@link XmlParser}
*/
protected IParser getParser(String format) {
if (StringUtils.isBlank(format)) {
format = ResourceFormat.RESOURCE_XML.getHeader();
}
if (format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) {
return new JsonParser();
} else if (format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) {
return new XmlParser();
} else {
throw new EFhirClientException("Invalid format: " + format);
}
}
/**
* Logs the given {@link Response}, using the current {@link ToolingClientLogger}. If the current
* {@link FhirRequestBuilder#logger} is null, no action is taken.
*
* @param responseCode HTTP response code
* @param responseHeaders {@link Headers} from response
* @param responseBody Byte array response
*/
protected void log(int responseCode, Headers responseHeaders, byte[] responseBody) {
if (logger != null) {
List<String> headerList = new ArrayList<>(Collections.emptyList());
Map<String, List<String>> headerMap = responseHeaders.toMultimap();
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
try {
logger.logResponse(Integer.toString(responseCode), headerList, responseBody);
} catch (Exception e) {
System.out.println("Error parsing response body passed in to logger ->\n" + e.getLocalizedMessage());
}
}
// else { // TODO fix logs
// System.out.println("Call to log HTTP response with null ToolingClientLogger set... are you forgetting to " +
// "initialize your logger?");
// }
}
}

View File

@ -0,0 +1,71 @@
package org.hl7.fhir.r5.utils.client.network;
/*
Copyright (c) 2011+, HL7, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of HL7 nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
import org.hl7.fhir.r5.model.Resource;
public class ResourceRequest<T extends Resource> {
private T payload;
private int httpStatus = -1;
private String location;
public ResourceRequest(T payload, int httpStatus, String location) {
this.payload = payload;
this.httpStatus = httpStatus;
this.location = location;
}
public int getHttpStatus() {
return httpStatus;
}
public T getPayload() {
return payload;
}
public T getReference() {
T payloadResource = null;
if (payload != null) {
payloadResource = payload;
}
return payloadResource;
}
public boolean isSuccessfulRequest() {
return this.httpStatus / 100 == 2;
}
public boolean isUnsuccessfulRequest() {
return !isSuccessfulRequest();
}
public String getLocation() {
return location;
}
}

View File

@ -0,0 +1,63 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
/**
* An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the number of times we retry a to execute a
* given request, before reporting a failure. This includes unsuccessful return codes and timeouts.
*/
public class RetryInterceptor implements Interceptor {
// Delay between retying failed requests, in millis
private final long RETRY_TIME = 2000;
// Maximum number of times to retry the request before failing
private final int maxRetry;
// Internal counter for tracking the number of times we've tried this request
private int retryCounter = 0;
public RetryInterceptor(int maxRetry) {
this.maxRetry = maxRetry;
}
@Override
public @NotNull Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
do {
try {
// If we are retrying a failed request that failed due to a bad response from the server, we must close it first
if (response != null) {
System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code())
+ "> from url -> " + chain.request().url() + ".");
response.close();
}
// System.out.println(chain.request().method() + " attempt <" + (retryCounter + 1) + "> to url -> " + chain.request().url());
response = chain.proceed(request);
} catch (IOException e) {
try {
// Include a small break in between requests.
Thread.sleep(RETRY_TIME);
} catch (InterruptedException e1) {
System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" + retryCounter + ">");
}
} finally {
retryCounter++;
}
} while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1));
/*
* if something has gone wrong, and we are unable to complete the request, we still need to initialize the return
* response so we don't get a null pointer exception.
*/
return response != null ? response : chain.proceed(request);
}
}

View File

@ -0,0 +1,146 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.model.*;
import org.junit.jupiter.api.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ClientTest {
private static final long TIMEOUT = 5000;
private MockWebServer server;
private HttpUrl serverUrl;
private Client client;
private Address address = new Address()
.setCity("Toronto")
.setState("Ontario")
.setCountry("Canada");
private HumanName humanName = new HumanName()
.addGiven("Mark")
.setFamily("Iantorno");
private Patient patient = new Patient()
.addName(humanName)
.addAddress(address)
.setGender(Enumerations.AdministrativeGender.MALE);
@BeforeEach
void setup() {
setupMockServer();
client = new Client();
}
void setupMockServer() {
server = new MockWebServer();
serverUrl = server.url("/v1/endpoint");
}
byte[] generateResourceBytes(Resource resource) throws IOException {
return new JsonParser().composeBytes(resource);
}
@Test
@DisplayName("GET request, happy path.")
void test_get_happy_path() throws IOException, URISyntaxException {
server.enqueue(
new MockResponse()
.setBody(new String(generateResourceBytes(patient)))
);
ResourceRequest<Resource> resourceRequest = client.issueGetResourceRequest(new URI(serverUrl.toString()),
"json", null, TIMEOUT);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");
}
@Test
@DisplayName("GET request, test client retries after timeout failure.")
void test_get_retries_with_timeout() throws IOException, URISyntaxException {
int failedAttempts = new Random().nextInt(5) + 1;
System.out.println("Simulating <" + failedAttempts + "> failed connections (timeouts) before success.");
for (int i = 0; i < failedAttempts; i++) {
server.enqueue(
new MockResponse()
.setHeadersDelay(TIMEOUT * 10, TimeUnit.MILLISECONDS)
.setBody(new String(generateResourceBytes(patient)))
);
}
server.enqueue(new MockResponse().setBody(new String(generateResourceBytes(patient))));
client.setRetryCount(failedAttempts + 1);
ResourceRequest<Resource> resourceRequest = client.issueGetResourceRequest(new URI(serverUrl.toString()),
"json", null, TIMEOUT);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");
}
@Test
@DisplayName("GET request, test client retries after bad response.")
void test_get_retries_with_unsuccessful_response() throws IOException, URISyntaxException {
int failedAttempts = new Random().nextInt(5) + 1;
System.out.println("Simulating <" + failedAttempts + "> failed connections (bad response codes) before success.");
for (int i = 0; i < failedAttempts; i++) {
server.enqueue(
new MockResponse()
.setResponseCode(400 + i)
.setBody(new String(generateResourceBytes(patient)))
);
}
server.enqueue(new MockResponse().setBody(new String(generateResourceBytes(patient))));
client.setRetryCount(failedAttempts + 1);
ResourceRequest<Resource> resourceRequest = client.issueGetResourceRequest(new URI(serverUrl.toString()),
"json", null, TIMEOUT);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");
}
@Test
@DisplayName("PUT request, test payload received by server matches sent.")
void test_put() throws IOException, URISyntaxException, InterruptedException {
byte[] payload = ByteUtils.resourceToByteArray(patient, true, false);
// Mock server response of 200, with the same resource payload returned that we included in the PUT request
server.enqueue(
new MockResponse()
.setResponseCode(200)
.setBody(new String(payload))
);
ResourceRequest<Resource> request = client.issuePutRequest(new URI(serverUrl.toString()), payload,
"xml", null, TIMEOUT);
RecordedRequest recordedRequest = server.takeRequest();
Assertions.assertArrayEquals(payload, recordedRequest.getBody().readByteArray(),
"PUT request payload does not match send data.");
}
@Test
@DisplayName("POST request, test payload received by server matches sent.")
void test_post() throws IOException, URISyntaxException, InterruptedException {
byte[] payload = ByteUtils.resourceToByteArray(patient, true, false);
// Mock server response of 200, with the same resource payload returned that we included in the PUT request
server.enqueue(
new MockResponse()
.setResponseCode(200)
.setBody(new String(payload))
);
ResourceRequest<Resource> request = client.issuePostRequest(new URI(serverUrl.toString()), payload,
"xml", null, TIMEOUT);
RecordedRequest recordedRequest = server.takeRequest();
Assertions.assertArrayEquals(payload, recordedRequest.getBody().readByteArray(),
"POST request payload does not match send data.");
}
}

View File

@ -0,0 +1,147 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.hl7.fhir.r5.model.OperationOutcome;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class FhirRequestBuilderTest {
@Test
@DisplayName("Test default headers are added correctly.")
void addDefaultHeaders() {
Request.Builder request = new Request.Builder().url("http://www.google.com");
FhirRequestBuilder.addDefaultHeaders(request);
Map<String, List<String>> headersMap = request.build().headers().toMultimap();
Assertions.assertNotNull(headersMap.get("User-Agent"), "User-Agent header null.");
Assertions.assertEquals("hapi-fhir-tooling-client", headersMap.get("User-Agent").get(0),
"User-Agent header not populated with expected value \"hapi-fhir-tooling-client\".");
Assertions.assertNotNull(headersMap.get("Accept-Charset"), "Accept-Charset header null.");
Assertions.assertEquals(FhirRequestBuilder.DEFAULT_CHARSET, headersMap.get("Accept-Charset").get(0),
"Accept-Charset header not populated with expected value " + FhirRequestBuilder.DEFAULT_CHARSET);
}
@Test
@DisplayName("Test resource format headers are added correctly.")
void addResourceFormatHeaders() {
String testFormat = "yaml";
Request.Builder request = new Request.Builder().url("http://www.google.com");
FhirRequestBuilder.addResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = request.build().headers().toMultimap();
Assertions.assertNotNull(headersMap.get("Accept"), "Accept header null.");
Assertions.assertEquals(testFormat, headersMap.get("Accept").get(0),
"Accept header not populated with expected value " + testFormat + ".");
Assertions.assertNotNull(headersMap.get("Content-Type"), "Content-Type header null.");
Assertions.assertEquals(testFormat + ";charset=" + FhirRequestBuilder.DEFAULT_CHARSET, headersMap.get("Content-Type").get(0),
"Content-Type header not populated with expected value \"" + testFormat + ";charset=" + FhirRequestBuilder.DEFAULT_CHARSET + "\".");
}
@Test
@DisplayName("Test a list of provided headers are added correctly.")
void addHeaders() {
String headerName1 = "headerName1";
String headerValue1 = "headerValue1";
String headerName2 = "headerName2";
String headerValue2 = "headerValue2";
Headers headers = new Headers.Builder()
.add(headerName1, headerValue1)
.add(headerName2, headerValue2)
.build();
Request.Builder request = new Request.Builder().url("http://www.google.com");
FhirRequestBuilder.addHeaders(request, headers);
Map<String, List<String>> headersMap = request.build().headers().toMultimap();
Assertions.assertNotNull(headersMap.get(headerName1), headerName1 + " header null.");
Assertions.assertEquals(headerValue1, headersMap.get(headerName1).get(0),
headerName1 + " header not populated with expected value " + headerValue1 + ".");
Assertions.assertNotNull(headersMap.get(headerName2), headerName2 + " header null.");
Assertions.assertEquals(headerValue2, headersMap.get(headerName2).get(0),
headerName2 + " header not populated with expected value " + headerValue2 + ".");
}
@Test
@DisplayName("Test that FATAL issue severity triggers error.")
void hasErrorTestFatal() {
OperationOutcome outcome = new OperationOutcome();
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.FATAL));
Assertions.assertTrue(FhirRequestBuilder.hasError(outcome), "Error check not triggered for FATAL issue severity.");
}
@Test
@DisplayName("Test that ERROR issue severity triggers error.")
void hasErrorTestError() {
OperationOutcome outcome = new OperationOutcome();
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.ERROR));
Assertions.assertTrue(FhirRequestBuilder.hasError(outcome), "Error check not triggered for ERROR issue severity.");
}
@Test
@DisplayName("Test that no FATAL or ERROR issue severity does not trigger error.")
void hasErrorTestNoErrors() {
OperationOutcome outcome = new OperationOutcome();
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL));
outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING));
Assertions.assertFalse(FhirRequestBuilder.hasError(outcome), "Error check triggered unexpectedly.");
}
@Test
@DisplayName("Test that getLocationHeader returns header for 'location'.")
void getLocationHeaderWhenOnlyLocationIsSet() {
final String expectedLocationHeader = "location_header_value";
Headers headers = new Headers.Builder()
.add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader)
.build();
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@Test
@DisplayName("Test that getLocationHeader returns header for 'content-location'.")
void getLocationHeaderWhenOnlyContentLocationIsSet() {
final String expectedContentLocationHeader = "content_location_header_value";
Headers headers = new Headers.Builder()
.add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)
.build();
Assertions.assertEquals(expectedContentLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@Test
@DisplayName("Test that getLocationHeader returns 'location' header when both 'location' and 'content-location' are set.")
void getLocationHeaderWhenLocationAndContentLocationAreSet() {
final String expectedLocationHeader = "location_header_value";
final String expectedContentLocationHeader = "content_location_header_value";
Headers headers = new Headers.Builder()
.add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader)
.add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)
.build();
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@Test
@DisplayName("Test that getLocationHeader returns null when no location available.")
void getLocationHeaderWhenNoLocationSet() {
Headers headers = new Headers.Builder()
.build();
Assertions.assertNull(FhirRequestBuilder.getLocationHeader(headers));
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -83,14 +83,25 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -778,7 +778,7 @@ public class Utilities {
public static String encodeUri(String v) {
return v.replace(" ", "%20").replace("?", "%3F").replace("=", "%3D");
return v.replace(" ", "%20").replace("?", "%3F").replace("=", "%3D").replace("|", "%7C");
}

View File

@ -455,4 +455,12 @@ public class VersionUtilities {
return res;
}
public static String getVersionForPackage(String pid) {
if (pid.startsWith("hl7.fhir.r")) {
String[] p = pid.split("\\.");
return versionFromCode(p[2]);
}
return null;
}
}

View File

@ -27,6 +27,7 @@ public class I18nConstants {
public static final String BUNDLE_BUNDLE_ENTRY_NOFIRST = "Bundle_BUNDLE_Entry_NoFirst";
public static final String BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE = "Bundle_BUNDLE_Entry_NoFirstResource";
public static final String BUNDLE_BUNDLE_ENTRY_NOFULLURL = "Bundle_BUNDLE_Entry_NoFullUrl";
public static final String BUNDLE_BUNDLE_ENTRY_FULLURL_REQUIRED = "BUNDLE_BUNDLE_ENTRY_FULLURL_REQUIRED";
public static final String BUNDLE_BUNDLE_ENTRY_NOPROFILE = "Bundle_BUNDLE_Entry_NoProfile";
public static final String BUNDLE_BUNDLE_ENTRY_MULTIPLE_PROFILES = "BUNDLE_BUNDLE_ENTRY_MULTIPLE_PROFILES";
public static final String BUNDLE_BUNDLE_ENTRY_NOTFOUND = "Bundle_BUNDLE_Entry_NotFound";
@ -349,6 +350,7 @@ public class I18nConstants {
public static final String SD_ED_TYPE_PROFILE_UNKNOWN = "SD_ED_TYPE_PROFILE_UNKNOWN";
public static final String SD_ED_TYPE_PROFILE_NOTYPE = "SD_ED_TYPE_PROFILE_NOTYPE";
public static final String SD_ED_TYPE_PROFILE_WRONG = "SD_ED_TYPE_PROFILE_WRONG";
public static final String SD_ED_TYPE_NO_TARGET_PROFILE = "SD_ED_TYPE_NO_TARGET_PROFILE";
public static final String SEARCHPARAMETER_BASE_WRONG = "SEARCHPARAMETER_BASE_WRONG";
public static final String SEARCHPARAMETER_EXP_WRONG = "SEARCHPARAMETER_EXP_WRONG";
public static final String SEARCHPARAMETER_NOTFOUND = "SEARCHPARAMETER_NOTFOUND";
@ -572,6 +574,8 @@ public class I18nConstants {
public static final String VALIDATION_VAL_PROFILE_THIS_VERSION_OK = "VALIDATION_VAL_PROFILE_THIS_VERSION_OK";
public static final String VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER = "VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER";
public static final String VALIDATION_VAL_PROFILE_UNKNOWN = "Validation_VAL_Profile_Unknown";
public static final String VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY = "VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY";
public static final String VALIDATION_VAL_PROFILE_UNKNOWN_ERROR = "VALIDATION_VAL_PROFILE_UNKNOWN_ERROR";
public static final String VALIDATION_VAL_PROFILE_WRONGTYPE = "Validation_VAL_Profile_WrongType";
public static final String VALIDATION_VAL_PROFILE_WRONGTYPE2 = "Validation_VAL_Profile_WrongType2";
public static final String VALIDATION_VAL_UNKNOWN_PROFILE = "Validation_VAL_Unknown_Profile";

View File

@ -46,6 +46,7 @@ import org.hl7.fhir.utilities.npm.NpmPackage.NpmPackageFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@ -57,6 +58,7 @@ import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.security.cert.X509Certificate;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@ -219,6 +221,11 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple
return retVal;
}
retVal = super.loadFromPackageServer(id, VersionUtilities.getMajMin(version)+".x");
if (retVal != null) {
return retVal;
}
// ok, well, we'll try the old way
return fetchTheOldWay(id, version);
}
@ -523,7 +530,7 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple
InputStream stream = fetchFromUrlSpecific(Utilities.pathURL("http://build.fhir.org", id + ".tgz"), false);
return new InputStreamWithSrc(stream, Utilities.pathURL("http://build.fhir.org", id + ".tgz"), "current");
} else {
throw new FHIRException("The package '" + id + "' has not entry on the current build server");
throw new FHIRException("The package '" + id + "' has no entry on the current build server");
}
}
@ -624,13 +631,16 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple
loadFromBuildServer();
} catch (Exception e) {
System.out.println("Error connecting to build server - running without build (" + e.getMessage() + ")");
e.printStackTrace();
}
return false;
}
private void loadFromBuildServer() throws IOException {
URL url = new URL("https://build.fhir.org/ig/qas.json?nocache=" + System.currentTimeMillis());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
SSLCertTruster.trustAllHosts();
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setHostnameVerifier(SSLCertTruster.DO_NOT_VERIFY);
connection.setRequestMethod("GET");
InputStream json = connection.getInputStream();
buildInfo = (JsonArray) new com.google.gson.JsonParser().parse(TextFile.streamToString(json));
@ -655,16 +665,6 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple
buildLoaded = true; // whether it succeeds or not
}
// private String buildPath(String url) {
// for (JsonElement e : buildInfo) {
// JsonObject j = (JsonObject) e;
// if (j.has("url") && (url.equals(j.get("url").getAsString()) || j.get("url").getAsString().startsWith(url+"/ImplementationGuide"))) {
// return "https://build.fhir.org/ig/"+j.get("repo").getAsString();
// }
// }
// return null;
// }
//
private String getRepo(String path) {
String[] p = path.split("\\/");
return p[0] + "/" + p[1];

View File

@ -1087,6 +1087,12 @@ public class NpmPackage {
return false;
}
}
if (Utilities.existsInList(name(), "fhir.test.data.r2", "fhir.test.data.r3", "fhir.test.data.r4", "fhir.tx.support.r2", "fhir.tx.support.r3", "fhir.tx.support.r4", "us.nlm.vsac")) {
return true;
}
if (JSONUtil.bool(npm, "lazy-load")) {
return true;
}
if (!hasFile("other", "spec.internals")) {
return false;
}

View File

@ -0,0 +1,48 @@
package org.hl7.fhir.utilities.npm;
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
/**
* This is a _temporary_ fix to get around the fact that the build server's SSL certs have expired and people cannot
* publish IGs or run tests that rely on that box. The intention is to overhaul much of the current networking code
* to a more central, unified, HttpClient module.
* <p>
* If this is still in the code in 2021, contact markiantorno on github and yell at him.
*/
public class SSLCertTruster {
// always verify the host - dont check for certificate
final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
/**
* Trust every server - don't check for any certificate
*/
public static void trustAllHosts() {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}};
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -55,6 +55,7 @@ import org.xmlpull.v1.XmlPullParserException;
public class XhtmlParser {
public static final String XHTML_NS = "http://www.w3.org/1999/xhtml";
private static final char END_OF_CHARS = (char) -1;
public class NSMap {
private Map<String, String> nslist = new HashMap<String, String>();
@ -515,7 +516,7 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
private void parseElementInner(XhtmlNode node, List<XhtmlNode> parents, NSMap nsm, boolean escaping) throws FHIRFormatError, IOException
{
StringBuilder s = new StringBuilder();
while (peekChar() != '\0' && !parents.contains(unwindPoint) && !(node == unwindPoint))
while (peekChar() != END_OF_CHARS && !parents.contains(unwindPoint) && !(node == unwindPoint))
{
if (peekChar() == '<')
{
@ -606,7 +607,7 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
{
while (Character.isWhitespace(peekChar()))
readChar();
while (peekChar() != '>' && peekChar() != '/' && peekChar() != '\0')
while (peekChar() != '>' && peekChar() != '/' && peekChar() != END_OF_CHARS)
{
String name = readName();
if (name.length() == 0)
@ -630,7 +631,7 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
if (peekChar() == '"' || peekChar() == '\'')
node.getAttributes().put(name, parseAttributeValue(readChar()));
else
node.getAttributes().put(name, parseAttributeValue('\0'));
node.getAttributes().put(name, parseAttributeValue(END_OF_CHARS));
}
while (Character.isWhitespace(peekChar()))
readChar();
@ -640,7 +641,7 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
private String parseAttributeValue(char term) throws IOException, FHIRFormatError
{
StringBuilder b = new StringBuilder();
while (peekChar() != '\0' && peekChar() != '>' && (term != '\0' || peekChar() != '/') && peekChar() != term)
while (peekChar() != END_OF_CHARS && peekChar() != '>' && (term != END_OF_CHARS || peekChar() != '/') && peekChar() != term)
{
if (peekChar() == '&')
{
@ -704,15 +705,15 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
if (cache.length() > 0)
return cache.charAt(0);
else if (!rdr.ready())
return '\0';
return END_OF_CHARS;
else
{
char c = (char)rdr.read();
if (c == (char)-1)
{
int i = rdr.read();
if (i == -1) {
cache = "";
return '\0';
return END_OF_CHARS;
}
char c = (char) i;
cache = Character.toString(c);
return c;
}
@ -727,7 +728,7 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
cache = cache.length() == 1 ? "" : cache.substring(1);
}
else if (!rdr.ready())
c = '\0';
c = END_OF_CHARS;
else
c = (char)rdr.read();
if (c == '\r' || c == '\n') {
@ -744,9 +745,9 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
private String readToTagEnd() throws IOException, FHIRFormatError
{
StringBuilder s = new StringBuilder();
while (peekChar() != '>' && peekChar() != '\0')
while (peekChar() != '>' && peekChar() != END_OF_CHARS)
s.append(readChar());
if (peekChar() != '\0')
if (peekChar() != END_OF_CHARS)
{
readChar();
skipWhiteSpace();
@ -765,7 +766,7 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
if (c == '>') {
done = true;
readChar();
} else if (c != '\0')
} else if (c != END_OF_CHARS)
s.append(readChar());
else if (mustBeWellFormed)
throw new FHIRFormatError("Unexpected termination of html source"+descLoc());
@ -814,12 +815,12 @@ private boolean elementIsOk(String name) throws FHIRFormatError {
} else if (c == '[' && s.toString().startsWith("DOCTYPE ")) {
doctypeEntities = true;
s.append(readChar());
} else if (c != '\0')
} else if (c != END_OF_CHARS)
s.append(readChar());
else if (mustBeWellFormed)
throw new FHIRFormatError("Unexpected termination of html source"+descLoc());
}
if (peekChar() != '\0')
if (peekChar() != END_OF_CHARS)
{
readChar();
skipWhiteSpace();

View File

@ -7,6 +7,7 @@ Bundle_BUNDLE_Entry_MismatchIdUrl = The canonical URL ({0}) cannot match the ful
Bundle_BUNDLE_Entry_NoFirst = Documents or Messages must contain at least one entry
Bundle_BUNDLE_Entry_NoFirstResource = No resource on first entry
Bundle_BUNDLE_Entry_NoFullUrl = Bundle entry missing fullUrl
BUNDLE_BUNDLE_ENTRY_FULLURL_REQUIRED = Except for transactions and batches, each entry in a Bundle must have a fullUrl which is the identity of the resource in the entry
Bundle_BUNDLE_Entry_NoProfile = No profile found for contained resource of type ''{0}''
Bundle_BUNDLE_Entry_NotFound = Can''t find ''{0}'' in the bundle ({1})
Bundle_BUNDLE_Entry_Orphan = Entry {0} isn''t reachable by traversing from first Bundle entry
@ -223,7 +224,9 @@ Validation_VAL_Profile_NotAllowed = This element is not allowed by the profile {
Validation_VAL_Profile_NotSlice = This element does not match any known slice {0} and slicing is CLOSED: {1}
Validation_VAL_Profile_OutOfOrder = As specified by profile {0}, Element ''{1}'' is out of order
Validation_VAL_Profile_SliceOrder = As specified by profile {0}, Element ''{1}'' is out of order in ordered slice
Validation_VAL_Profile_Unknown = Profile reference ''{0}'' could not be resolved, so has not been checked
Validation_VAL_Profile_Unknown = Profile reference ''{0}'' has not been checked because it is unknown
VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY = Profile reference ''{0}'' has not been checked because it is unknown, and the validator is set to not fetch unknown profiles
VALIDATION_VAL_PROFILE_UNKNOWN_ERROR = Profile reference ''{0}'' has not been checked because it is unknown, and fetching it resulted in the error {1}
Validation_VAL_Profile_WrongType = Specified profile type was ''{0}'', but found type ''{1}''
Validation_VAL_Unknown_Profile = Unknown profile {0}
XHTML_XHTML_Attribute_Illegal = Illegal attribute name in the XHTML (''{0}'' on ''{1}'')
@ -502,8 +505,8 @@ Validation_VAL_Profile_WrongType = Specified profile type was ''{0}'', but found
Validation_VAL_Profile_WrongType2 = Type mismatch processing profile {0} at path {1}: The element type is {4}, but the profile {3} is for a different type {2}
VALIDATION_VAL_ILLEGAL_TYPE_CONSTRAINT = Illegal constraint in profile {0} at path {1} - cannot constrain to type {2} from base types {3}
EXTENSION_EXT_CONTEXT_WRONG_XVER = The extension {0} from FHIR version {3} is not allowed to be used at this point (allowed = {1}; this element is [{2}; this is a warning since contexts may be renamed between FHIR versions)
SECURITY_STRING_CONTENT_ERROR = The string value contains embedded HTML tags, which are not allowed for security reasons in this context
SECURITY_STRING_CONTENT_WARNING = The string value contains embedded HTML tags. Note that all inputs should be escaped when rendered to HTML as a matter of course
SECURITY_STRING_CONTENT_ERROR = The string value contains text that looks like embedded HTML tags, which are not allowed for security reasons in this context
SECURITY_STRING_CONTENT_WARNING = The string value contains text that looks like embedded HTML tags. If this content is rendered to HTML withour appropraite post-processing, it may be a security risk
ALL_OK = All OK
SEARCHPARAMETER_NOTFOUND = Unable to find the base Search Parameter {0} so can''t check that this SearchParameter is a proper derivation from it
SEARCHPARAMETER_BASE_WRONG = The resource type {1} is not listed as a base in the SearchParameter this is derived from ({0})
@ -615,7 +618,8 @@ SD_NESTED_MUST_SUPPORT_SNAPSHOT = The element {0} has types/profiles/targets tha
Unable_to_connect_to_terminology_server = Unable to connect to terminology server. Error = {0}
SD_ED_TYPE_PROFILE_UNKNOWN = Unable to resolve profile {0}
SD_ED_TYPE_PROFILE_NOTYPE = Found profile {0}, but unable to determine the type it applies it
SD_ED_TYPE_PROFILE_WRONG = Profile {0} is for type {1}, but this element has type {2}
SD_ED_TYPE_PROFILE_WRONG = Profile {0} is for type {1}, but the {3} element has type {2}
SD_ED_TYPE_NO_TARGET_PROFILE = Type {0} does not allow for target Profiles
TERMINOLOGY_TX_NOSVC_BOUND_REQ = Could not confirm that the codes provided are from the required value set {0} because there is no terminology service
TERMINOLOGY_TX_NOSVC_BOUND_EXT = Could not confirm that the codes provided are from the extensible value set {0} because there is no terminology service
ARRAY_CANNOT_BE_EMPTY = Array cannot be empty - the property should not be present if it has no values

View File

@ -1,6 +1,8 @@
package org.hl7.fhir.utilities.tests;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.hl7.fhir.utilities.xhtml.XhtmlParser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
@ -88,4 +90,9 @@ public class XhtmlNodeTest {
ObjectOutputStream oout = new ObjectOutputStream(bout);
oout.writeObject(node);
}
@Test
public void testParseBadChars() throws FHIRFormatError, IOException {
XhtmlNode x = new XhtmlParser().parse(BaseTestingUtilities.loadTestResource("xhtml", "bad-chars.html"), "div");
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -149,6 +149,20 @@
</lifecycleMappingMetadata>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -73,8 +73,11 @@ import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.DomainResource;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -118,7 +121,18 @@ public class BaseValidator {
protected Source source;
protected IWorkerContext context;
protected TimeTracker timeTracker = new TimeTracker();
protected XVerExtensionManager xverManager;
public BaseValidator(IWorkerContext context, XVerExtensionManager xverManager) {
super();
this.context = context;
this.xverManager = xverManager;
if (this.xverManager == null) {
this.xverManager = new XVerExtensionManager(context);
}
}
/**
* Use to control what validation the validator performs.
* Using this, you can turn particular kinds of validation on and off
@ -129,11 +143,7 @@ public class BaseValidator {
*/
private Map<String, ValidationControl> validationControl = new HashMap<>();
public BaseValidator(IWorkerContext context){
this.context = context;
}
/**
/**
* Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails
*
* @param thePass
@ -873,5 +883,74 @@ public class BaseValidator {
return validationControl;
}
public XVerExtensionStatus xverStatus(String url) {
return xverManager.status(url);
}
public boolean isXverUrl(String url) {
return xverManager.matchingUrl(url);
}
public StructureDefinition xverDefn(String url) {
return xverManager.makeDefinition(url);
}
public String xverVersion(String url) {
return xverManager.getVersion(url);
}
public String xverElementId(String url) {
return xverManager.getElementId(url);
}
public StructureDefinition getXverExt(StructureDefinition profile, List<ValidationMessage> errors, String url) {
if (isXverUrl(url)) {
switch (xverStatus(url)) {
case BadVersion:
rule(errors, IssueType.BUSINESSRULE, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_INVALID, url, xverVersion(url));
return null;
case Unknown:
rule(errors, IssueType.BUSINESSRULE, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_INVALIDID, url, xverElementId(url));
return null;
case Invalid:
rule(errors, IssueType.BUSINESSRULE, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_NOCHANGE, url, xverElementId(url));
return null;
case Valid:
StructureDefinition defn = xverDefn(url);
context.generateSnapshot(defn);
context.cacheResource(defn);
return defn;
default:
rule(errors, IssueType.INVALID, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_INTERNAL, url);
return null;
}
} else {
return null;
}
}
public StructureDefinition getXverExt(List<ValidationMessage> errors, String path, Element element, String url) {
if (isXverUrl(url)) {
switch (xverStatus(url)) {
case BadVersion:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INVALID, url, xverVersion(url));
break;
case Unknown:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INVALIDID, url, xverElementId(url));
break;
case Invalid:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_NOCHANGE, url, xverElementId(url));
break;
case Valid:
StructureDefinition ex = xverDefn(url);
context.generateSnapshot(ex);
context.cacheResource(ex);
return ex;
default:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INTERNAL, url);
break;
}
}
return null;
}
}

View File

@ -68,6 +68,7 @@ import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.formats.XmlParser;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Constants;
import org.hl7.fhir.r5.model.DomainResource;
@ -1007,7 +1008,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst
}
public void loadIg(String src, boolean recursive) throws IOException, FHIRException {
NpmPackage npm = src.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX_OPT) ? pcm.loadPackage(src, null) : null;
NpmPackage npm = src.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX_OPT) && !new File(src).exists() ? pcm.loadPackage(src, null) : null;
if (npm != null) {
for (String s : npm.dependencies()) {
if (!context.getLoadedPackages().contains(s)) {
@ -1563,7 +1564,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst
}
public InstanceValidator getValidator() {
InstanceValidator validator = new InstanceValidator(context, null);
InstanceValidator validator = new InstanceValidator(context, null, null);
validator.setHintAboutNonMustSupport(hintAboutNonMustSupport);
validator.setAnyExtensionsAllowed(anyExtensionsAllowed);
validator.setNoInvariantChecks(isNoInvariantChecks());
@ -1907,6 +1908,17 @@ public class ValidationEngine implements IValidatorResourceFetcher, IPackageInst
this.locale = locale;
}
@Override
public CanonicalResource fetchCanonicalResource(String url) throws URISyntaxException {
return fetcher != null ? fetcher.fetchCanonicalResource(url) : null;
}
@Override
public boolean fetchesCanonicalResource(String url) {
return fetcher != null ? fetcher.fetchesCanonicalResource(url) : false;
}
public void handleOutput(Resource r, String output, String version) throws FHIRException, IOException {
if (output.startsWith("http://") || output.startsWith("http://")) {
ByteArrayOutputStream bs = new ByteArrayOutputStream();

View File

@ -68,6 +68,11 @@ import org.hl7.fhir.validation.cli.services.ValidationService;
import org.hl7.fhir.validation.cli.utils.*;
import java.io.File;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import java.util.Base64;
/**
* A executable class that will validate one or more FHIR resources against
@ -86,6 +91,11 @@ public class ValidatorCli {
public static final String HTTP_PROXY_HOST = "http.proxyHost";
public static final String HTTP_PROXY_PORT = "http.proxyPort";
public static final String HTTP_PROXY_USER = "http.proxyUser";
public static final String HTTP_PROXY_PASS = "http.proxyPassword";
public static final String JAVA_DISABLED_TUNNELING_SCHEMES = "jdk.http.auth.tunneling.disabledSchemes";
public static final String JAVA_DISABLED_PROXY_SCHEMES = "jdk.http.auth.proxying.disabledSchemes";
public static final String JAVA_USE_SYSTEM_PROXIES = "java.net.useSystemProxies";
public static void main(String[] args) throws Exception {
TimeTracker tt = new TimeTracker();
@ -95,11 +105,45 @@ public class ValidatorCli {
Display.displaySystemInfo();
if (Params.hasParam(args, Params.PROXY)) {
String[] p = Params.getParam(args, Params.PROXY).split("\\:");
assert Params.getParam(args, Params.PROXY) != null : "PROXY arg passed in was NULL";
String[] p = Params.getParam(args, Params.PROXY).split(":");
System.setProperty(HTTP_PROXY_HOST, p[0]);
System.setProperty(HTTP_PROXY_PORT, p[1]);
}
if (Params.hasParam(args, Params.PROXY_AUTH)) {
assert Params.getParam(args, Params.PROXY) != null : "Cannot set PROXY_AUTH without setting PROXY...";
assert Params.getParam(args, Params.PROXY_AUTH) != null : "PROXY_AUTH arg passed in was NULL...";
String[] p = Params.getParam(args, Params.PROXY_AUTH).split(":");
String authUser = p[0];
String authPass = p[1];
/*
* For authentication, use java.net.Authenticator to set proxy's configuration and set the system properties
* http.proxyUser and http.proxyPassword
*/
Authenticator.setDefault(
new Authenticator() {
@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(authUser, authPass.toCharArray());
}
}
);
System.setProperty(HTTP_PROXY_USER, authUser);
System.setProperty(HTTP_PROXY_PASS, authPass);
System.setProperty(JAVA_USE_SYSTEM_PROXIES, "true");
/*
* For Java 1.8 and higher you must set
* -Djdk.http.auth.tunneling.disabledSchemes=
* to make proxies with Basic Authorization working with https along with Authenticator
*/
System.setProperty(JAVA_DISABLED_TUNNELING_SCHEMES, "");
System.setProperty(JAVA_DISABLED_PROXY_SCHEMES, "");
}
CliContext cliContext = Params.loadCliContext(args);
if (Params.hasParam(args, Params.TEST)) {

View File

@ -2,13 +2,17 @@ package org.hl7.fhir.validation.cli.services;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.Locale;
import org.hl7.fhir.convertors.txClient.TerminologyClientFactory;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.IResourceValidator.IValidatorResourceFetcher;
import org.hl7.fhir.r5.utils.IResourceValidator.ReferenceValidationPolicy;
import org.hl7.fhir.utilities.Utilities;
@ -38,12 +42,12 @@ public class StandAloneValidatorFetcher implements IValidatorResourceFetcher {
@Override
public Element fetch(Object appContext, String url) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
throw new Error("Not done yet");
throw new FHIRException("The URL '"+url+"' is not known to the FHIR validator, and has not been provided as part of the setup / parameters");
}
@Override
public ReferenceValidationPolicy validationPolicy(Object appContext, String path, String url) {
throw new Error("Not done yet");
return ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS;
}
@Override
@ -111,12 +115,40 @@ public class StandAloneValidatorFetcher implements IValidatorResourceFetcher {
@Override
public byte[] fetchRaw(String url) throws MalformedURLException, IOException {
throw new Error("Not done yet");
throw new FHIRException("The URL '"+url+"' is not known to the FHIR validator, and has not been provided as part of the setup / parameters");
}
@Override
public void setLocale(Locale locale) {
throw new Error("Not done yet");
// nothing
}
@Override
public CanonicalResource fetchCanonicalResource(String url) throws URISyntaxException {
String[] p = url.split("\\/");
String root = getRoot(p, url);
if (root != null) {
TerminologyClient c;
c = TerminologyClientFactory.makeClient(root, context.getVersion());
return c.read(p[p.length-2], p[p.length-1]);
} else {
throw new FHIRException("The URL '"+url+"' is not known to the FHIR validator, and has not been provided as part of the setup / parameters");
}
}
private String getRoot(String[] p, String url) {
if (p.length > 3 && Utilities.isValidId(p[p.length-1]) && context.getResourceNames().contains(p[p.length-2])) {
url = url.substring(0, url.lastIndexOf("/"));
return url.substring(0, url.lastIndexOf("/"));
} else {
return null;
}
}
@Override
public boolean fetchesCanonicalResource(String url) {
return true;
}
}

View File

@ -1,11 +1,13 @@
package org.hl7.fhir.validation.cli.utils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.ToolsVersion;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* Class for displaying output to the cli user.
@ -32,9 +34,9 @@ public class Display {
*/
public static void displayHelpDetails() {
ClassLoader classLoader = Display.class.getClassLoader();
File file = new File(classLoader.getResource("help.txt").getFile());
InputStream help = classLoader.getResourceAsStream("help.txt");
try {
String data = FileUtils.readFileToString(file, "UTF-8");
String data = IOUtils.toString(help, "UTF-8");
System.out.println(data);
} catch (IOException e) {
e.printStackTrace();

View File

@ -1,5 +1,6 @@
package org.hl7.fhir.validation.cli.utils;
import org.apache.http.auth.AUTH;
import org.hl7.fhir.r5.utils.IResourceValidator.BundleValidationRule;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.validation.cli.model.CliContext;
@ -14,6 +15,7 @@ public class Params {
public static final String OUTPUT = "-output";
public static final String HTML_OUTPUT = "-html-output";
public static final String PROXY = "-proxy";
public static final String PROXY_AUTH = "-auth";
public static final String PROFILE = "-profile";
public static final String BUNDLE = "-bundle";
public static final String QUESTIONNAIRE = "-questionnaire";
@ -101,6 +103,8 @@ public class Params {
cliContext.setHtmlOutput(args[++i]);
} else if (args[i].equals(PROXY)) {
i++; // ignore next parameter
} else if (args[i].equals(PROXY_AUTH)) {
i++;
} else if (args[i].equals(PROFILE)) {
String p = null;
if (i + 1 == args.length) {
@ -234,8 +238,6 @@ public class Params {
cliContext.addSource(args[i]);
}
}
if (cliContext.getSources().isEmpty())
throw new Exception("Must provide at least one source file");
return cliContext;
}

View File

@ -39,14 +39,15 @@ import java.util.Set;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
import org.hl7.fhir.validation.BaseValidator;
public class CodeSystemValidator extends BaseValidator {
public CodeSystemValidator(IWorkerContext context) {
super(context);
public CodeSystemValidator(IWorkerContext context, XVerExtensionManager xverManager) {
super(context, xverManager);
}
public List<ValidationMessage> validate(CodeSystem cs, boolean forBuild) {

View File

@ -368,7 +368,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private EnableWhenEvaluator myEnableWhenEvaluator = new EnableWhenEvaluator();
private String executionId;
private XVerExtensionManager xverManager;
private IValidationProfileUsageTracker tracker;
private ValidatorHostServices validatorServices;
private boolean assumeValidRestReferences;
@ -380,8 +379,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private boolean validateValueSetCodesOnTxServer = true;
private QuestionnaireMode questionnaireMode;
public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) {
super(theContext);
public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices, XVerExtensionManager xverManager) {
super(theContext, xverManager);
this.externalHostServices = hostServices;
this.profileUtilities = new ProfileUtilities(theContext, null, null);
fpe = new FHIRPathEngine(context);
@ -1504,30 +1503,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
StructureDefinition ex = Utilities.isAbsoluteUrl(url) ? context.fetchResource(StructureDefinition.class, url) : null;
timeTracker.sd(t);
if (ex == null) {
if (xverManager == null) {
xverManager = new XVerExtensionManager(context);
}
if (xverManager.matchingUrl(url)) {
switch (xverManager.status(url)) {
case BadVersion:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INVALID, url, xverManager.getVersion(url));
break;
case Unknown:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INVALIDID, url, xverManager.getElementId(url));
break;
case Invalid:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_NOCHANGE, url, xverManager.getElementId(url));
break;
case Valid:
ex = xverManager.makeDefinition(url);
context.generateSnapshot(ex);
context.cacheResource(ex);
break;
default:
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INTERNAL, url);
break;
}
} else if (extensionUrl != null && !isAbsolute(url)) {
ex = getXverExt(errors, path, element, url);
}
if (ex == null) {
if (extensionUrl != null && !isAbsolute(url)) {
if (extensionUrl.equals(profile.getUrl())) {
rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", hasExtensionSlice(profile, url), I18nConstants.EXTENSION_EXT_SUBEXTENSION_INVALID, url, profile.getUrl());
}
@ -1568,6 +1547,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return ex;
}
private boolean hasExtensionSlice(StructureDefinition profile, String sliceName) {
for (ElementDefinition ed : profile.getSnapshot().getElement()) {
@ -1951,7 +1931,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
boolean found;
try {
found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url) ||
SpecialExtensions.isKnownExtension(url);
SpecialExtensions.isKnownExtension(url) || isXverUrl(url);
} catch (IOException e1) {
found = false;
}
@ -2446,25 +2426,26 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (pol.checkExists()) {
if (we == null) {
if (fetcher == null) {
if (!refType.equals("contained"))
if (!refType.equals("contained")) {
if (fetcher == null) {
throw new FHIRException(context.formatMessage(I18nConstants.RESOURCE_RESOLUTION_SERVICES_NOT_PROVIDED));
} else {
Element ext = null;
if (fetchCache.containsKey(ref)) {
ext = fetchCache.get(ref);
} else {
try {
ext = fetcher.fetch(hostContext.getAppContext(), ref);
} catch (IOException e) {
throw new FHIRException(e);
}
if (ext != null) {
setParents(ext);
fetchCache.put(ref, ext);
Element ext = null;
if (fetchCache.containsKey(ref)) {
ext = fetchCache.get(ref);
} else {
try {
ext = fetcher.fetch(hostContext.getAppContext(), ref);
} catch (IOException e) {
throw new FHIRException(e);
}
if (ext != null) {
setParents(ext);
fetchCache.put(ref, ext);
}
}
we = ext == null ? null : makeExternalRef(ext, path);
}
we = ext == null ? null : makeExternalRef(ext, path);
}
}
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, (allowExamples && (ref.contains("example.org") || ref.contains("acme.com"))) || (we != null || pol == ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS), I18nConstants.REFERENCE_REF_CANTRESOLVE, ref);
@ -3760,10 +3741,29 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
StructureDefinition sdt = context.fetchResource(StructureDefinition.class, vu.getUrl());
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER, sdt == null ? "null" : sdt.getType());
}
} else if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", sd != null, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN, profile.primitiveValue())) {
signpost(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), !crumbTrails, I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_META, sd.getUrl());
stack.resetIds();
startInner(hostContext, errors, resource, element, sd, stack, false);
} else {
if (sd == null) {
// we'll try fetching it directly from it's source, but this is likely to fail later even if the resolution succeeds
if (fetcher == null) {
warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN, profile.primitiveValue());
} else if (!fetcher.fetchesCanonicalResource(profile.primitiveValue())) {
warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY, profile.primitiveValue());
} else {
try {
sd = (StructureDefinition) fetcher.fetchCanonicalResource(profile.primitiveValue());
} catch (Exception e) {
warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_ERROR, profile.primitiveValue(), e.getMessage());
}
if (sd != null) {
context.cacheResource(sd);
}
}
}
if (sd != null) {
signpost(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), !crumbTrails, I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_META, sd.getUrl());
stack.resetIds();
startInner(hostContext, errors, resource, element, sd, stack, false);
}
}
}
i++;
@ -3869,27 +3869,27 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
public void checkSpecials(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack, boolean checkSpecials) {
// specific known special validations
if (element.getType().equals(BUNDLE)) {
new BundleValidator(context, serverBase, this).validateBundle(errors, element, stack, checkSpecials, hostContext);
new BundleValidator(context, serverBase, this, xverManager).validateBundle(errors, element, stack, checkSpecials, hostContext);
} else if (element.getType().equals("Observation")) {
validateObservation(errors, element, stack);
} else if (element.getType().equals("Questionnaire")) {
new QuestionnaireValidator(context, myEnableWhenEvaluator, fpe, timeTracker, questionnaireMode).validateQuestionannaire(errors, element, element, stack);
new QuestionnaireValidator(context, myEnableWhenEvaluator, fpe, timeTracker, questionnaireMode, xverManager).validateQuestionannaire(errors, element, element, stack);
} else if (element.getType().equals("QuestionnaireResponse")) {
new QuestionnaireValidator(context, myEnableWhenEvaluator, fpe, timeTracker, questionnaireMode).validateQuestionannaireResponse(hostContext, errors, element, stack);
new QuestionnaireValidator(context, myEnableWhenEvaluator, fpe, timeTracker, questionnaireMode, xverManager).validateQuestionannaireResponse(hostContext, errors, element, stack);
} else if (element.getType().equals("Measure")) {
new MeasureValidator(context, timeTracker).validateMeasure(hostContext, errors, element, stack);
new MeasureValidator(context, timeTracker, xverManager).validateMeasure(hostContext, errors, element, stack);
} else if (element.getType().equals("MeasureReport")) {
new MeasureValidator(context, timeTracker).validateMeasureReport(hostContext, errors, element, stack);
new MeasureValidator(context, timeTracker, xverManager).validateMeasureReport(hostContext, errors, element, stack);
} else if (element.getType().equals("CapabilityStatement")) {
validateCapabilityStatement(errors, element, stack);
} else if (element.getType().equals("CodeSystem")) {
new CodeSystemValidator(context, timeTracker).validateCodeSystem(errors, element, stack);
new CodeSystemValidator(context, timeTracker, xverManager).validateCodeSystem(errors, element, stack);
} else if (element.getType().equals("SearchParameter")) {
new SearchParameterValidator(context, timeTracker, fpe).validateSearchParameter(errors, element, stack);
new SearchParameterValidator(context, timeTracker, fpe, xverManager).validateSearchParameter(errors, element, stack);
} else if (element.getType().equals("StructureDefinition")) {
new StructureDefinitionValidator(context, timeTracker, fpe, wantCheckSnapshotUnchanged).validateStructureDefinition(errors, element, stack);
new StructureDefinitionValidator(context, timeTracker, fpe, wantCheckSnapshotUnchanged, xverManager).validateStructureDefinition(errors, element, stack);
} else if (element.getType().equals("ValueSet")) {
new ValueSetValidator(context, timeTracker, this).validateValueSet(errors, element, stack);
new ValueSetValidator(context, timeTracker, this, xverManager).validateValueSet(errors, element, stack);
}
}

View File

@ -36,12 +36,18 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.context.SimpleWorkerContext.IValidatorFactory;
import org.hl7.fhir.r5.utils.IResourceValidator;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
public class InstanceValidatorFactory implements IValidatorFactory {
@Override
public IResourceValidator makeValidator(IWorkerContext ctxt, XVerExtensionManager xverManager) throws FHIRException {
return new InstanceValidator(ctxt, null, xverManager);
}
@Override
public IResourceValidator makeValidator(IWorkerContext ctxt) throws FHIRException {
return new InstanceValidator(ctxt, null);
return new InstanceValidator(ctxt, null, null);
}
}

View File

@ -13,6 +13,7 @@ import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.Constants;
import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.r5.utils.IResourceValidator.BundleValidationRule;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
@ -31,8 +32,8 @@ public class BundleValidator extends BaseValidator{
private String serverBase;
private InstanceValidator validator;
public BundleValidator(IWorkerContext context, String serverBase, InstanceValidator validator) {
super(context);
public BundleValidator(IWorkerContext context, String serverBase, InstanceValidator validator, XVerExtensionManager xverManager) {
super(context, xverManager);
this.serverBase = serverBase;
this.validator = validator;
}
@ -42,7 +43,7 @@ public class BundleValidator extends BaseValidator{
bundle.getNamedChildren(ENTRY, entries);
String type = bundle.getNamedChildValue(TYPE);
type = StringUtils.defaultString(type);
if (entries.size() == 0) {
rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals(DOCUMENT) || type.equals(MESSAGE)), I18nConstants.BUNDLE_BUNDLE_ENTRY_NOFIRST);
} else {
@ -79,6 +80,8 @@ public class BundleValidator extends BaseValidator{
int count = 0;
Map<String, Integer> counter = new HashMap<>();
boolean fullUrlOptional = Utilities.existsInList(type, "transaction", "transaction-response", "batch", "batch-response");
for (Element entry : entries) {
NodeStack estack = stack.push(entry, count, null, null);
String fullUrl = entry.getNamedChildValue(FULL_URL);
@ -90,6 +93,9 @@ public class BundleValidator extends BaseValidator{
rule(errors, IssueType.INVALID, entry.line(), entry.col(), stack.addToLiteralPath(ENTRY, PATH_ARG), !url.equals(fullUrl) || serverBase == null || (url.equals(Utilities.pathURL(serverBase, entry.getNamedChild(RESOURCE).fhirType(), id))), I18nConstants.BUNDLE_BUNDLE_ENTRY_CANONICAL, url, fullUrl);
}
if (!VersionUtilities.isR2Ver(context.getVersion())) {
rule(errors, IssueType.INVALID, entry.line(), entry.col(), estack.getLiteralPath(), fullUrlOptional || fullUrl != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_FULLURL_REQUIRED);
}
// check bundle profile requests
if (entry.hasChild(RESOURCE)) {
String rtype = entry.getNamedChild(RESOURCE).fhirType();

View File

@ -6,6 +6,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -17,8 +18,8 @@ import org.hl7.fhir.validation.instance.utils.NodeStack;
public class CodeSystemValidator extends BaseValidator {
public CodeSystemValidator(IWorkerContext context, TimeTracker timeTracker) {
super(context);
public CodeSystemValidator(IWorkerContext context, TimeTracker timeTracker, XVerExtensionManager xverManager) {
super(context, xverManager);
source = Source.InstanceValidator;
this.timeTracker = timeTracker;
}

View File

@ -26,6 +26,7 @@ import org.hl7.fhir.r5.model.Measure.MeasureGroupPopulationComponent;
import org.hl7.fhir.r5.model.Measure.MeasureGroupStratifierComponent;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.renderers.DataRenderer;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -40,8 +41,8 @@ import org.w3c.dom.Document;
public class MeasureValidator extends BaseValidator {
public MeasureValidator(IWorkerContext context, TimeTracker timeTracker) {
super(context);
public MeasureValidator(IWorkerContext context, TimeTracker timeTracker, XVerExtensionManager xverManager) {
super(context, xverManager);
source = Source.InstanceValidator;
this.timeTracker = timeTracker;
}

View File

@ -35,6 +35,7 @@ import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.model.TimeType;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
@ -58,8 +59,8 @@ public class QuestionnaireValidator extends BaseValidator {
private FHIRPathEngine fpe;
private QuestionnaireMode questionnaireMode;
public QuestionnaireValidator(IWorkerContext context, EnableWhenEvaluator myEnableWhenEvaluator, FHIRPathEngine fpe, TimeTracker timeTracker, QuestionnaireMode questionnaireMode) {
super(context);
public QuestionnaireValidator(IWorkerContext context, EnableWhenEvaluator myEnableWhenEvaluator, FHIRPathEngine fpe, TimeTracker timeTracker, QuestionnaireMode questionnaireMode, XVerExtensionManager xverManager) {
super(context, xverManager);
source = Source.InstanceValidator;
this.myEnableWhenEvaluator = myEnableWhenEvaluator;
this.fpe = fpe;

View File

@ -12,6 +12,7 @@ import org.hl7.fhir.r5.model.ExpressionNode.Kind;
import org.hl7.fhir.r5.model.ExpressionNode.Operation;
import org.hl7.fhir.r5.model.SearchParameter;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -34,8 +35,8 @@ public class SearchParameterValidator extends BaseValidator {
private FHIRPathEngine fpe;
public SearchParameterValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe) {
super(context);
public SearchParameterValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, XVerExtensionManager xverManager) {
super(context, xverManager);
source = Source.InstanceValidator;
this.fpe = fpe;
this.timeTracker = timeTracker;

View File

@ -20,9 +20,11 @@ import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.ElementDefinition;
import org.hl7.fhir.r5.model.ExpressionNode;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -46,8 +48,8 @@ public class StructureDefinitionValidator extends BaseValidator {
private FHIRPathEngine fpe;
private boolean wantCheckSnapshotUnchanged;
public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, boolean wantCheckSnapshotUnchanged) {
super(context);
public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, boolean wantCheckSnapshotUnchanged, XVerExtensionManager xverManager) {
super(context, xverManager);
source = Source.InstanceValidator;
this.fpe = fpe;
this.timeTracker = timeTracker;
@ -66,6 +68,7 @@ public class StructureDefinitionValidator extends BaseValidator {
if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
List<ValidationMessage> msgs = new ArrayList<>();
ProfileUtilities pu = new ProfileUtilities(context, msgs, null);
pu.setXver(xverManager);
pu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir", sd.getName());
if (msgs.size() > 0) {
for (ValidationMessage msg : msgs) {
@ -137,12 +140,50 @@ public class StructureDefinitionValidator extends BaseValidator {
}
if (code != null) {
List<Element> profiles = type.getChildrenByName("profile");
for (Element profile : profiles) {
validateTypeProfile(errors, profile, code, stack.push(type, -1, null, null));
if (VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion()) ) {
for (Element profile : profiles) {
validateProfileTypeOrTarget(errors, profile, code, stack.push(profile, -1, null, null), path);
}
} else {
for (Element profile : profiles) {
validateTypeProfile(errors, profile, code, stack.push(profile, -1, null, null), path);
}
profiles = type.getChildrenByName("targetProfile");
for (Element profile : profiles) {
validateTargetProfile(errors, profile, code, stack.push(profile, -1, null, null), path);
}
}
}
}
private void validateProfileTypeOrTarget(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
String p = profile.primitiveValue();
StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
if (code.equals("Reference")) {
if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
String t = determineBaseType(sd);
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
}
}
} else {
if (sd == null ) {
sd = getXverExt(errors, stack.getLiteralPath(), profile, p);
}
if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
String t = determineBaseType(sd);
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
}
}
}
}
private String getTypeCodeFromSD(StructureDefinition sd, String path) {
ElementDefinition ed = null;
for (ElementDefinition t : sd.getSnapshot().getElement()) {
@ -157,19 +198,48 @@ public class StructureDefinitionValidator extends BaseValidator {
return ed != null && ed.getType().size() == 1 ? ed.getTypeFirstRep().getCode() : null;
}
private void validateTypeProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack) {
private void validateTypeProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
String p = profile.primitiveValue();
StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
if (sd == null ) {
sd = getXverExt(errors, stack.getLiteralPath(), profile, p);
}
if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
String t = determineBaseType(sd);
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code);
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
}
}
}
private void validateTargetProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
String p = profile.primitiveValue();
StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
if (code.equals("Reference")) {
if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
String t = determineBaseType(sd);
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
}
}
} else if (code.equals("canonical")) {
if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
String t = determineBaseType(sd);
if (t == null) {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), code.equals(t), I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
}
}
} else {
rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_NO_TARGET_PROFILE, code);
}
}
private boolean isInstanceOf(String t, String code) {
StructureDefinition sd = context.fetchTypeDefinition(t);
while (sd != null) {

View File

@ -11,6 +11,7 @@ import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
@ -42,8 +43,8 @@ public class ValueSetValidator extends BaseValidator {
private InstanceValidator parent;
public ValueSetValidator(IWorkerContext context, TimeTracker timeTracker, InstanceValidator parent) {
super(context);
public ValueSetValidator(IWorkerContext context, TimeTracker timeTracker, InstanceValidator parent, XVerExtensionManager xverManager) {
super(context, xverManager);
source = Source.InstanceValidator;
this.timeTracker = timeTracker;
this.parent = parent;

View File

@ -40,7 +40,9 @@ import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionConstraintCompon
import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
import org.hl7.fhir.validation.BaseValidator;
@ -50,8 +52,8 @@ public class ProfileValidator extends BaseValidator {
private boolean checkAggregation = false;
private boolean checkMustSupport = false;
public ProfileValidator(IWorkerContext context) {
super(context);
public ProfileValidator(IWorkerContext context, XVerExtensionManager xverManager) {
super(context, xverManager);
}
public boolean isCheckAggregation() {
@ -138,7 +140,12 @@ public class ProfileValidator extends BaseValidator {
if (!ec.getType().isEmpty() && "Extension".equals(ec.getType().get(0).getWorkingCode()) && ec.getType().get(0).hasProfile()) {
String url = ec.getType().get(0).getProfile().get(0).getValue();
StructureDefinition defn = context.fetchResource(StructureDefinition.class, url);
if (defn == null) {
defn = getXverExt(profile, errors, url);
}
rule(errors, IssueType.BUSINESSRULE, profile.getId(), defn != null, "Unable to find Extension '"+url+"' referenced at "+profile.getUrl()+" "+kind+" "+ec.getPath()+" ("+ec.getSliceName()+")");
}
}
}

View File

@ -52,10 +52,12 @@ import org.hl7.fhir.r5.utils.IResourceValidator.ReferenceValidationPolicy;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.json.JSONUtil;
import org.hl7.fhir.utilities.npm.NpmPackage;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.hl7.fhir.validation.ValidationEngine;
import org.hl7.fhir.validation.cli.services.StandAloneValidatorFetcher;
import org.hl7.fhir.validation.instance.InstanceValidator;
import org.junit.AfterClass;
import org.junit.Assert;
@ -81,8 +83,9 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
Map<String, JsonObject> examples = new HashMap<String, JsonObject>();
manifest = (JsonObject) new com.google.gson.JsonParser().parse(contents);
for (Entry<String, JsonElement> e : manifest.getAsJsonObject("test-cases").entrySet()) {
examples.put(e.getKey(), e.getValue().getAsJsonObject());
for (JsonElement e : manifest.getAsJsonArray("test-cases")) {
JsonObject o = (JsonObject) e;
examples.put(JSONUtil.str(o, "name"), o);
}
List<String> names = new ArrayList<String>(examples.size());
@ -145,7 +148,6 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
throw new Exception("unknown version " + version);
}
vCurr = ve.get(version);
vCurr.setFetcher(this);
if (TestingUtilities.fcontexts == null) {
TestingUtilities.fcontexts = new HashMap<>();
}
@ -154,11 +156,17 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
if (content.has("use-test") && !content.get("use-test").getAsBoolean())
return;
String testCaseContent = TestingUtilities.loadTestResource("validator", name);
String testCaseContent = TestingUtilities.loadTestResource("validator", JSONUtil.str(content, "file"));
InstanceValidator val = vCurr.getValidator();
val.setWantCheckSnapshotUnchanged(true);
val.getContext().setClientRetryCount(4);
val.setDebug(false);
if (content.has("fetcher") && "standalone".equals(JSONUtil.str(content, "fetcher"))) {
val.setFetcher(vCurr);
vCurr.setFetcher(new StandAloneValidatorFetcher(vCurr.getPcm(), vCurr.getContext(), vCurr));
} else {
val.setFetcher(this);
}
if (content.has("allowed-extension-domain"))
val.getExtensionDomains().add(content.get("allowed-extension-domain").getAsString());
if (content.has("allowed-extension-domains"))
@ -168,7 +176,6 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
val.setValidationLanguage(content.get("language").getAsString());
else
val.setValidationLanguage(null);
val.setFetcher(this);
if (content.has("packages")) {
for (JsonElement e : content.getAsJsonArray("packages")) {
String n = e.getAsString();
@ -226,7 +233,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
if (content.has("logical")==false) {
val.setAssumeValidRestReferences(content.has("assumeValidRestReferences") ? content.get("assumeValidRestReferences").getAsBoolean() : false);
System.out.println(String.format("Start Validating (%d to set up)", (System.nanoTime() - setup) / 1000000));
if (name.endsWith(".json"))
if (JSONUtil.str(content, "file").endsWith(".json"))
val.validate(null, errors, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.JSON);
else
val.validate(null, errors, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.XML);
@ -258,7 +265,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
val.getContext().cacheResource(sd);
val.setAssumeValidRestReferences(profile.has("assumeValidRestReferences") ? profile.get("assumeValidRestReferences").getAsBoolean() : false);
List<ValidationMessage> errorsProfile = new ArrayList<ValidationMessage>();
if (name.endsWith(".json"))
if (JSONUtil.str(content, "file").endsWith(".json"))
val.validate(null, errorsProfile, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.JSON, asSdList(sd));
else
val.validate(null, errorsProfile, IOUtils.toInputStream(testCaseContent, Charsets.UTF_8), FhirFormat.XML, asSdList(sd));
@ -524,4 +531,14 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
URLConnection c = url.openConnection();
return TextFile.streamToBytes(c.getInputStream());
}
@Override
public CanonicalResource fetchCanonicalResource(String url) {
return null;
}
@Override
public boolean fetchesCanonicalResource(String url) {
return false;
}
}

View File

@ -11,15 +11,15 @@
<!--
Note: Version of this project and the version of HAPI FHIR are not locked to
each other. It is fine to bump the point version of this POM without affecting
HAPI FHIR.
HAPI FHIR
-->
<artifactId>org.hl7.fhir.core</artifactId>
<version>5.1.23-SNAPSHOT</version>
<version>5.2.8-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<hapi_fhir_version>5.1.0</hapi_fhir_version>
<validator_test_case_version>1.1.50</validator_test_case_version>
<validator_test_case_version>1.1.55</validator_test_case_version>
<junit_jupiter_version>5.6.2</junit_jupiter_version>
<maven_surefire_version>3.0.0-M4</maven_surefire_version>
<jacoco_version>0.8.5</jacoco_version>