Merge branch 'master' into 2024-11-gg-slice-validation

This commit is contained in:
Grahame Grieve 2024-11-09 08:20:36 +10:30 committed by GitHub
commit 275df1aba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 3907 additions and 3020 deletions

View File

@ -8,7 +8,9 @@
* Fix special case r5 loading of terminology to fix validation error on ExampleScenario
* Improve handling of JSON format errors
* Fix bug where extension slices defined in other profiles are not found when processing slices based on extension
* Validate slice expressions where possible
* Validate fhirpath expression in slice discriminators
* Fix slicing by type and profile to allow multiple options per slice
* List measure choices when a match by version can't be found
## Other code changes

View File

@ -1,6 +1,5 @@
package org.hl7.fhir.convertors.misc;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.HashSet;
@ -395,7 +394,7 @@ public class ICD11Generator {
private JsonObject fetchJson(String source) throws IOException {
HTTPResult res = ManagedWebAccess.builder().withAccept("application/json").withHeader("API-Version", "v2").withHeader("Accept-Language", "en").get(source);
HTTPResult res = ManagedWebAccess.accessor().withHeader("API-Version", "v2").withHeader("Accept-Language", "en").get(source,"application/json");
res.checkThrowException();
return JsonParser.parseObject(res.getContent());
}

View File

@ -32,6 +32,7 @@ import org.hl7.fhir.r4.terminologies.JurisdictionUtilities;
import org.hl7.fhir.utilities.CSVReader;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
import org.hl7.fhir.utilities.http.ManagedWebAccess;
import org.hl7.fhir.utilities.json.model.JsonArray;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.model.JsonProperty;
@ -45,17 +46,16 @@ public class VSACImporter extends OIDBasedValueSetImporter {
public static void main(String[] args) throws FHIRException, IOException, ParseException, URISyntaxException {
VSACImporter self = new VSACImporter();
self.process(args[0], args[1], args[2], "true".equals(args[3]), "true".equals(args[4]));
self.process(args[0], args[1], "true".equals(args[2]), "true".equals(args[3]));
}
private void process(String source, String dest, String apiKey, boolean onlyNew, boolean onlyActive) throws FHIRException, IOException, URISyntaxException {
private void process(String source, String dest, boolean onlyNew, boolean onlyActive) throws FHIRException, IOException, URISyntaxException {
CSVReader csv = new CSVReader(ManagedFileAccess.inStream(source));
csv.readHeaders();
Map<String, String> errs = new HashMap<>();
ManagedWebAccess.loadFromFHIRSettings();
FHIRToolingClient fhirToolingClient = new FHIRToolingClient("https://cts.nlm.nih.gov/fhir", "fhir/vsac");
fhirToolingClient.setUsername("apikey");
fhirToolingClient.setPassword(apiKey);
fhirToolingClient.setTimeoutNormal(30000);
fhirToolingClient.setTimeoutExpand(30000);
@ -121,6 +121,7 @@ public class VSACImporter extends OIDBasedValueSetImporter {
oo.addIssue().setSeverity(IssueSeverity.ERROR).setCode(IssueType.EXCEPTION).setDiagnostics(errs.get(oid)).addLocation(oid);
}
new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "other", "OperationOutcome-vsac-errors.json")), oo);
System.out.println();
System.out.println("Done. " + i + " ValueSets in "+Utilities.describeDuration(System.currentTimeMillis() - tt));
}

View File

@ -28,6 +28,12 @@
<artifactId>org.hl7.fhir.utilities</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- UCUM -->
<dependency>
<groupId>org.fhir</groupId>
@ -81,6 +87,24 @@
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>

View File

@ -33,42 +33,21 @@ 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 java.util.*;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
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.dstu2.formats.IParser;
import org.hl7.fhir.dstu2.formats.IParser.OutputStyle;
import org.hl7.fhir.dstu2.formats.JsonParser;
@ -84,8 +63,11 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.MimeType;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.http.*;
import org.hl7.fhir.utilities.settings.FhirSettings;
import javax.annotation.Nonnull;
/**
* Helper class handling lower level HTTP transport concerns. TODO Document
* methods.
@ -93,51 +75,35 @@ import org.hl7.fhir.utilities.settings.FhirSettings;
* @author Claude Nanjo
*/
public class ClientUtils {
protected static final String LOCATION_HEADER = "location";
protected static final String CONTENT_LOCATION_HEADER = "content-location";
public static final String DEFAULT_CHARSET = "UTF-8";
public static final String HEADER_LOCATION = "location";
private static boolean debugging = false;
private HttpHost proxy;
@Getter
@Setter
private int timeout = 5000;
private String username;
private String password;
@Setter
@Getter
private ToolingClientLogger logger;
@Setter
@Getter
private int retryCount;
@Getter
@Setter
private String userAgent;
private String acceptLang;
private String contentLang;
@Setter
private String acceptLanguage;
@Setter
private String contentLanguage;
private final TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
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;
protected ManagedFhirWebAccessor getManagedWebAccessor() {
return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
}
public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat,
@ -146,8 +112,10 @@ public class ClientUtils {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpOptions options = new HttpOptions(optionsUri);
return issueResourceRequest(resourceFormat, options, timeoutLoading);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.OPTIONS)
.withUrl(optionsUri.toString());
return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat,
@ -155,17 +123,23 @@ public class ClientUtils {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpGet httpget = new HttpGet(resourceUri);
return issueResourceRequest(resourceFormat, httpget, timeoutLoading);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.GET)
.withUrl(resourceUri.toString());
return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
List<Header> headers, int timeoutLoading) {
Iterable<HTTPHeader> headers, int timeoutLoading) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpPut httpPut = new HttpPut(resourceUri);
return issueResourceRequest(resourceFormat, httpPut, payload, headers, timeoutLoading);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.PUT)
.withUrl(resourceUri.toString())
.withBody(payload);
return issueResourceRequest(resourceFormat, httpRequest, headers, timeoutLoading);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
@ -173,17 +147,25 @@ public class ClientUtils {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpPut httpPut = new HttpPut(resourceUri);
return issueResourceRequest(resourceFormat, httpPut, payload, null, timeoutLoading);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.PUT)
.withUrl(resourceUri.toString())
.withBody(payload);
return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload,
String resourceFormat, List<Header> headers, int timeoutLoading) {
String resourceFormat, Iterable<HTTPHeader> headers, int timeoutLoading) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpPost httpPost = new HttpPost(resourceUri);
return issueResourceRequest(resourceFormat, httpPost, payload, headers, timeoutLoading);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.POST)
.withUrl(resourceUri.toString())
.withBody(payload);
return issueResourceRequest(resourceFormat, httpRequest, headers, timeoutLoading);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload,
@ -195,30 +177,26 @@ public class ClientUtils {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
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) {
}
}
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.GET)
.withUrl(resourceUri.toString());
Iterable<HTTPHeader> headers = getFhirHeaders(httpRequest, resourceFormat);
HTTPResult response = sendRequest(httpRequest.withHeaders(headers));
return unmarshalReference(response, resourceFormat);
}
public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, int timeoutLoading) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpPost httpPost = new HttpPost(resourceUri);
configureFhirRequest(httpPost, resourceFormat);
HttpResponse response = sendPayload(httpPost, payload, proxy, timeoutLoading);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.POST)
.withUrl(resourceUri.toString())
.withBody(payload);
Iterable<HTTPHeader> headers = getFhirHeaders(httpRequest, resourceFormat);
HTTPResult response = sendPayload(httpRequest.withHeaders(headers));
return unmarshalFeed(response, resourceFormat);
}
@ -226,9 +204,12 @@ public class ClientUtils {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpDelete deleteRequest = new HttpDelete(resourceUri);
HttpResponse response = sendRequest(deleteRequest);
int responseStatusCode = response.getStatusLine().getStatusCode();
HTTPRequest request = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.DELETE)
.withUrl(resourceUri.toString());
HTTPResult response = sendRequest(request);
int responseStatusCode = response.getCode();
boolean deletionSuccessful = false;
if (responseStatusCode == 204) {
deletionSuccessful = true;
@ -240,171 +221,120 @@ public class ClientUtils {
* Request/Response Helper methods
***********************************************************/
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request,
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HTTPRequest request,
int timeoutLoading) {
return issueResourceRequest(resourceFormat, request, null, timeoutLoading);
return issueResourceRequest(resourceFormat, request, Collections.emptyList(), timeoutLoading);
}
/**
* @param resourceFormat
* @param options
* @return
* Issue a resource request.
* @param resourceFormat the expected FHIR format
* @param request the request to be sent
* @param headers any additional headers to add
*
* @return A ResourceRequest object containing the requested resource
*/
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request,
byte[] payload, int timeoutLoading) {
return issueResourceRequest(resourceFormat, request, payload, null, timeoutLoading);
}
/**
* @param resourceFormat
* @param options
* @return
*/
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request,
byte[] payload, List<Header> headers, int timeoutLoading) {
protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HTTPRequest request,
@Nonnull Iterable<HTTPHeader> headers, int timeoutLoading) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
configureFhirRequest(request, resourceFormat, headers);
HttpResponse response = null;
if (request instanceof HttpEntityEnclosingRequest && payload != null) {
response = sendPayload((HttpEntityEnclosingRequestBase) request, payload, proxy, timeoutLoading);
} 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) {
if (!Utilities.noString(userAgent)) {
request.addHeader("User-Agent", userAgent);
}
if (!Utilities.noString(acceptLang)) {
request.addHeader("Accept-Language", acceptLang);
}
if (!Utilities.noString(contentLang)) {
request.addHeader("Content-Language", acceptLang);
}
if (format != null) {
request.addHeader("Accept", format);
request.addHeader("Content-Type", format + ";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,
int timeoutLoading) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpResponse response = null;
boolean ok = false;
long t = System.currentTimeMillis();
int tryCount = 0;
while (!ok) {
try {
tryCount++;
HttpClient httpclient = new DefaultHttpClient();
HttpParams params = httpclient.getParams();
HttpConnectionParams.setConnectionTimeout(params, timeout);
HttpConnectionParams.setSoTimeout(params, timeout * timeoutLoading);
if (proxy != null) {
httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
}
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) + ")");
if (tryCount <= retryCount || (tryCount < 3 && ioe instanceof org.apache.http.conn.ConnectTimeoutException)) {
ok = false;
} else {
throw new EFhirClientException("Error sending HTTP Post/Put Payload to " + "??" + ": " + ioe.getMessage(),
ioe);
}
}
}
return response;
}
/**
*
* @param request
* @param payload
* @return
*/
protected HttpResponse sendRequest(HttpUriRequest request) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HttpResponse response = null;
Iterable<HTTPHeader> configuredHeaders = getFhirHeaders(request, resourceFormat, headers);
try {
HttpClient httpclient = new DefaultHttpClient();
log(request);
HttpParams params = httpclient.getParams();
HttpConnectionParams.setConnectionTimeout(params, timeout);
HttpConnectionParams.setSoTimeout(params, timeout);
if (proxy != null) {
httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
}
response = httpclient.execute(request);
HTTPResult response = getManagedWebAccessor().httpCall(request.withHeaders(configuredHeaders));
T resource = unmarshalReference(response, resourceFormat);
return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
} catch (IOException ioe) {
throw new EFhirClientException("Error sending HTTP Post/Put Payload to " + "??" + ": " + ioe.getMessage(),
ioe);
}
}
/**
* Get required headers for FHIR requests.
*
* @param httpRequest the request
* @param format the expected format
*/
protected Iterable<HTTPHeader> getFhirHeaders(HTTPRequest httpRequest, String format) {
return getFhirHeaders(httpRequest, format, null);
}
/**
* Get required headers for FHIR requests.
*
* @param httpRequest the request
* @param format the expected format
* @param headers any additional headers to add
*/
protected Iterable<HTTPHeader> getFhirHeaders(HTTPRequest httpRequest, String format, Iterable<HTTPHeader> headers) {
List<HTTPHeader> configuredHeaders = new ArrayList<>();
if (!Utilities.noString(userAgent)) {
configuredHeaders.add(new HTTPHeader("User-Agent", userAgent));
}
if (!Utilities.noString(acceptLanguage)) {
configuredHeaders.add(new HTTPHeader("Accept-Language", acceptLanguage));
}
if (!Utilities.noString(contentLanguage)) {
configuredHeaders.add(new HTTPHeader("Content-Language", acceptLanguage));
}
Iterable<HTTPHeader> resourceFormatHeaders = getResourceFormatHeaders(httpRequest, format);
resourceFormatHeaders.forEach(configuredHeaders::add);
if (headers != null) {
headers.forEach(configuredHeaders::add);
}
return configuredHeaders;
}
protected static List<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
List<HTTPHeader> headers = new ArrayList<>();
headers.add(new HTTPHeader("Accept", format));
if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
) {
headers.add(new HTTPHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET));
}
return headers;
}
/**
*
* @param request The request to be sent
* @return The response from the server
*/
protected HTTPResult sendRequest(HTTPRequest request) {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
HTTPResult response = null;
try {
response = getManagedWebAccessor().httpCall(request);
return response;
} 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
* @param response The response from the server
* @return The unmarshalled resource
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(HttpResponse response, String format) {
protected <T extends Resource> T unmarshalReference(HTTPResult response, String format) {
T resource = null;
OperationOutcome error = null;
byte[] cnt = log(response);
if (cnt != null) {
if (response.getContent() != null) {
try {
resource = (T) getParser(format).parse(cnt);
resource = (T) getParser(format).parse(response.getContent());
if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) {
error = (OperationOutcome) resource;
}
@ -423,18 +353,18 @@ public class ClientUtils {
/**
* Unmarshals Bundle from response stream.
*
* @param response
* @return
* @param response The response from the server
* @return The unmarshalled Bundle
*/
protected Bundle unmarshalFeed(HttpResponse response, String format) {
protected Bundle unmarshalFeed(HTTPResult response, String format) {
Bundle feed = null;
byte[] cnt = log(response);
String contentType = response.getHeaders("Content-Type")[0].getValue();
String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
OperationOutcome error = null;
try {
if (cnt != null) {
if (response.getContent() != null) {
if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) {
Resource rf = getParser(format).parse(cnt);
Resource rf = getParser(format).parse(response.getContent());
if (rf instanceof Bundle)
feed = (Bundle) rf;
else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) {
@ -455,21 +385,20 @@ public class ClientUtils {
return feed;
}
private boolean hasError(OperationOutcome oo) {
protected 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();
protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
if (locationHeader != null) {
return locationHeader;
}
return location;
return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
}
/*****************************************************************
@ -586,12 +515,17 @@ public class ClientUtils {
public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName,
Resource resource, String resourceFormat) throws IOException {
HttpPost httppost = new HttpPost(resourceUri);
HTTPRequest httpRequest = new HTTPRequest()
.withMethod(HTTPRequest.HttpMethod.POST)
.withUrl(resourceUri.toString());
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));
List<HTTPHeader> headers = new ArrayList<>();
headers.add(new HTTPHeader("Content-Type", "multipart/form-data; boundary=" + boundary));
headers.add(new HTTPHeader("Accept", resourceFormat));
this.getFhirHeaders(httpRequest, null).forEach(headers::add);
HTTPResult response = sendPayload(httpRequest.withBody(encodeFormSubmission(parameters, resourceName, resource, boundary)).withHeaders(headers));
return unmarshalFeed(response, resourceFormat);
}
@ -622,80 +556,22 @@ public class ClientUtils {
}
/**
* Method posts request payload
* Send an HTTP Post/Put Payload
*
* @param request
* @param payload
* @return
* @param request The request to be sent
* @return The response from the server
*/
protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) {
HttpResponse response = null;
protected HTTPResult sendPayload(HTTPRequest request) {
HTTPResult response = null;
try {
log(request);
HttpClient httpclient = new DefaultHttpClient();
request.setEntity(new ByteArrayEntity(payload));
response = httpclient.execute(request);
log(response);
response = getManagedWebAccessor().httpCall(request);
} 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, 0);
}
return cnt;
}
public ToolingClientLogger getLogger() {
return logger;
}
public void setLogger(ToolingClientLogger logger) {
this.logger = logger;
}
/**
* Used for debugging
*
@ -714,26 +590,5 @@ public class ClientUtils {
return value;
}
public int getRetryCount() {
return retryCount;
}
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public void setAcceptLanguage(String language) {
this.acceptLang = language;
}
public void setContentLanguage(String language) {
this.contentLang = language;
}
}

View File

@ -95,48 +95,21 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
private HttpHost proxy;
private int maxResultSetSize = -1;// _count
private Conformance conf;
private ClientUtils utils = new ClientUtils();
private ClientUtils utils = null;
private int useCount;
protected ClientUtils getClientUtils() {
return new ClientUtils();
}
// Pass enpoint for client - URI
public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
preferredResourceFormat = ResourceFormat.RESOURCE_XML;
utils = getClientUtils();
utils.setUserAgent(userAgent);
detectProxy();
initialize(baseServiceUrl);
}
public FHIRToolingClient(String baseServiceUrl, String userAgent, String username, String password)
throws URISyntaxException {
preferredResourceFormat = ResourceFormat.RESOURCE_XML;
utils.setUserAgent(userAgent);
utils.setUsername(username);
utils.setPassword(password);
detectProxy();
initialize(baseServiceUrl);
}
public void configureProxy(String proxyHost, int proxyPort) {
utils.setProxy(new HttpHost(proxyHost, proxyPort));
}
public void detectProxy() {
String host = System.getenv(hostKey);
String port = System.getenv(portKey);
if (host == null) {
host = System.getProperty(hostKey);
}
if (port == null) {
port = System.getProperty(portKey);
}
if (host != null && port != null) {
this.configureProxy(host, Integer.parseInt(port));
}
}
public void initialize(String baseServiceUrl) throws URISyntaxException {
base = baseServiceUrl;
resourceAddress = new ResourceAddress(baseServiceUrl);
@ -286,11 +259,11 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
recordUse();
ResourceRequest<Resource> result = null;
try {
List<Header> headers = null;
result = utils.issuePutRequest(
resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())),
withVer(getPreferredResourceFormat(), "1.0"), headers, timeoutOperation);
withVer(getPreferredResourceFormat(), "1.0"), null, timeoutOperation);
result.addErrorStatus(410);// gone
result.addErrorStatus(404);// unknown
result.addErrorStatus(405);
@ -323,10 +296,12 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
recordUse();
ResourceRequest<T> result = null;
try {
List<Header> headers = null;
result = utils.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
result = utils.issuePutRequest(
resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())),
withVer(getPreferredResourceFormat(), "1.0"), headers, timeoutOperation);
withVer(getPreferredResourceFormat(), "1.0"),
null,
timeoutOperation);
result.addErrorStatus(410);// gone
result.addErrorStatus(404);// unknown
result.addErrorStatus(405);
@ -355,170 +330,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return result.getPayload();
}
//
// public <T extends Resource> boolean delete(Class<T> resourceClass, String id) {
// try {
// return utils.issueDeleteRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), proxy);
// } catch(Exception e) {
// throw new EFhirClientException("An error has occurred while trying to delete this resource", e);
// }
//
// }
//
// public <T extends Resource> OperationOutcome create(Class<T> resourceClass, T resource) {
// ResourceRequest<T> resourceRequest = null;
// try {
// List<Header> headers = null;
// resourceRequest = utils.issuePostRequest(resourceAddress.resolveGetUriFromResourceClass(resourceClass),utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), headers, proxy);
// resourceRequest.addSuccessStatus(201);
// if(resourceRequest.isUnsuccessfulRequest()) {
// throw new EFhirClientException("Server responded with HTTP error code " + resourceRequest.getHttpStatus(), (OperationOutcome)resourceRequest.getPayload());
// }
// } catch(Exception e) {
// handleException("An error has occurred while trying to create this resource", e);
// }
// OperationOutcome operationOutcome = null;;
// try {
// operationOutcome = (OperationOutcome)resourceRequest.getPayload();
// ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier =
// ResourceAddress.parseCreateLocation(resourceRequest.getLocation());
// OperationOutcomeIssueComponent issue = operationOutcome.addIssue();
// issue.setSeverity(IssueSeverity.INFORMATION);
// issue.setUserData(ResourceAddress.ResourceVersionedIdentifier.class.toString(),
// resVersionedIdentifier);
// return operationOutcome;
// } catch(ClassCastException e) {
// // some server (e.g. grahams) returns the resource directly
// operationOutcome = new OperationOutcome();
// OperationOutcomeIssueComponent issue = operationOutcome.addIssue();
// issue.setSeverity(IssueSeverity.INFORMATION);
// issue.setUserData(ResourceRequest.class.toString(),
// resourceRequest.getPayload());
// return operationOutcome;
// }
// }
//
// public <T extends Resource> Bundle history(Calendar lastUpdate, Class<T> resourceClass, String id) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceId(resourceClass, id, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history information for this resource", e);
// }
// return history;
// }
//
// public <T extends Resource> Bundle history(Date lastUpdate, Class<T> resourceClass, String id) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceId(resourceClass, id, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history information for this resource", e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history(Calendar lastUpdate, Class<T> resourceClass) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceType(resourceClass, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history information for this resource type", e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history(Date lastUpdate, Class<T> resourceClass) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceType(resourceClass, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history information for this resource type", e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history(Class<T> resourceClass) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceType(resourceClass, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history information for this resource type", e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history(Class<T> resourceClass, String id) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceId(resourceClass, id, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history information for this resource", e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history(Date lastUpdate) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForAllResources(lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history since last update",e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history(Calendar lastUpdate) {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForAllResources(lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history since last update",e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle history() {
// Bundle history = null;
// try {
// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForAllResources(maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("An error has occurred while trying to retrieve history since last update",e);
// }
// return history;
// }
//
//
// public <T extends Resource> Bundle search(Class<T> resourceClass, Map<String, String> parameters) {
// Bundle searchResults = null;
// try {
// searchResults = utils.issueGetFeedRequest(resourceAddress.resolveSearchUri(resourceClass, parameters), withVer(getPreferredResourceFormat(), "1.0"), proxy);
// } catch (Exception e) {
// handleException("Error performing search with parameters " + parameters, e);
// }
// return searchResults;
// }
//
//
// public <T extends Resource> Bundle searchPost(Class<T> resourceClass, T resource, Map<String, String> parameters) {
// Bundle searchResults = null;
// try {
// searchResults = utils.issuePostFeedRequest(resourceAddress.resolveSearchUri(resourceClass, new HashMap<String, String>()), parameters, "src", resource, getPreferredResourceFormat());
// } catch (Exception e) {
// handleException("Error performing search with parameters " + parameters, e);
// }
// return searchResults;
// }
public <T extends Resource> Parameters operateType(Class<T> resourceClass, String name, Parameters params) {
recordUse();
@ -588,107 +400,15 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return (OperationOutcome) result.getPayload();
}
/*
* change to meta operations
*
* public List<Coding> getAllTags() { TagListRequest result = null; try { result
* = utils.issueGetRequestForTagList(resourceAddress.resolveGetAllTags(),
* withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception e) {
* handleException("An error has occurred while trying to retrieve all tags",
* e); } return result.getPayload(); }
*
*
* public <T extends Resource> List<Coding> getAllTagsForResourceType(Class<T>
* resourceClass) { TagListRequest result = null; try { result =
* utils.issueGetRequestForTagList(resourceAddress.
* resolveGetAllTagsForResourceType(resourceClass),
* withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception e) {
* handleException("An error has occurred while trying to retrieve tags for this resource type"
* , e); } return result.getPayload(); }
*
*
* public <T extends Resource> List<Coding> getTagsForReference(Class<T>
* resource, String id) { TagListRequest result = null; try { result =
* utils.issueGetRequestForTagList(resourceAddress.resolveGetTagsForReference(
* resource, id), withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception
* e) {
* handleException("An error has occurred while trying to retrieve tags for this resource"
* , e); } return result.getPayload(); }
*
*
* public <T extends Resource> List<Coding> getTagsForResourceVersion(Class<T>
* resource, String id, String versionId) { TagListRequest result = null; try {
* result = utils.issueGetRequestForTagList(resourceAddress.
* resolveGetTagsForResourceVersion(resource, id, versionId),
* withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception e) {
* handleException("An error has occurred while trying to retrieve tags for this resource version"
* , e); } return result.getPayload(); }
*
* // // public <T extends Resource> boolean deleteTagsForReference(Class<T>
* resourceClass, String id) { // try { // return
* utils.issueDeleteRequest(resourceAddress.resolveGetTagsForReference(
* resourceClass, id), proxy); // } catch(Exception e) { //
* handleException("An error has occurred while trying to retrieve tags for this resource version"
* , e); // throw new
* EFhirClientException("An error has occurred while trying to delete this resource"
* , e); // } // // } // // // public <T extends Resource> boolean
* deleteTagsForResourceVersion(Class<T> resourceClass, String id, List<Coding>
* tags, String version) { // try { // return
* utils.issueDeleteRequest(resourceAddress.resolveGetTagsForResourceVersion(
* resourceClass, id, version), proxy); // } catch(Exception e) { //
* handleException("An error has occurred while trying to retrieve tags for this resource version"
* , e); // throw new
* EFhirClientException("An error has occurred while trying to delete this resource"
* , e); // } // }
*
*
* public <T extends Resource> List<Coding> createTags(List<Coding> tags,
* Class<T> resourceClass, String id) { TagListRequest request = null; try {
* request =
* utils.issuePostRequestForTagList(resourceAddress.resolveGetTagsForReference(
* resourceClass, id),utils.getTagListAsByteArray(tags, false,
* isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), null,
* proxy); request.addSuccessStatus(201); request.addSuccessStatus(200);
* if(request.isUnsuccessfulRequest()) { throw new
* EFhirClientException("Server responded with HTTP error code " +
* request.getHttpStatus()); } } catch(Exception e) {
* handleException("An error has occurred while trying to set tags for this resource"
* , e); } return request.getPayload(); }
*
*
* public <T extends Resource> List<Coding> createTags(List<Coding> tags,
* Class<T> resourceClass, String id, String version) { TagListRequest request =
* null; try { request = utils.issuePostRequestForTagList(resourceAddress.
* resolveGetTagsForResourceVersion(resourceClass, id,
* version),utils.getTagListAsByteArray(tags, false,
* isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), null,
* proxy); request.addSuccessStatus(201); request.addSuccessStatus(200);
* if(request.isUnsuccessfulRequest()) { throw new
* EFhirClientException("Server responded with HTTP error code " +
* request.getHttpStatus()); } } catch(Exception e) {
* handleException("An error has occurred while trying to set the tags for this resource version"
* , e); } return request.getPayload(); }
*
*
* public <T extends Resource> List<Coding> deleteTags(List<Coding> tags,
* Class<T> resourceClass, String id, String version) { TagListRequest request =
* null; try { request = utils.issuePostRequestForTagList(resourceAddress.
* resolveDeleteTagsForResourceVersion(resourceClass, id,
* version),utils.getTagListAsByteArray(tags, false,
* isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), null,
* proxy); request.addSuccessStatus(201); request.addSuccessStatus(200);
* if(request.isUnsuccessfulRequest()) { throw new
* EFhirClientException("Server responded with HTTP error code " +
* request.getHttpStatus()); } } catch(Exception e) {
* handleException("An error has occurred while trying to delete the tags for this resource version"
* , e); } return request.getPayload(); }
*/
/**
* Helper method to prevent nesting of previously thrown EFhirClientExceptions
*
* @param e
* @throws EFhirClientException
* Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of
* EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the
* cause.
*
* @param message The EFhirClientException message.
* @param e The exception.
* @throws EFhirClientException representing the exception.
*/
protected void handleException(String message, Exception e) throws EFhirClientException {
if (e instanceof EFhirClientException) {
@ -702,8 +422,8 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
* Helper method to determine whether desired resource representation is Json or
* XML.
*
* @param format
* @return
* @param format the format to check
* @return true if JSON, false if XML
*/
protected boolean isJson(String format) {
boolean isJson = false;
@ -784,13 +504,13 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
public ValueSet expandValueset(ValueSet source, Parameters expParams) {
recordUse();
List<Header> headers = null;
Parameters p = expParams == null ? new Parameters() : expParams.copy();
p.addParameter().setName("valueSet").setResource(source);
ResourceRequest<Resource> result = utils.issuePostRequest(
resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
utils.getResourceAsByteArray(p, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"),
headers, 4);
null, 4);
result.addErrorStatus(410); // gone
result.addErrorStatus(404); // unknown
result.addErrorStatus(405);
@ -812,11 +532,11 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
recordUse();
Parameters params = new Parameters();
params.addParameter().setName("name").setValue(new StringType(name));
List<Header> headers = null;
ResourceRequest<Resource> result = utils.issuePostRequest(
resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
utils.getResourceAsByteArray(params, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"),
headers, timeoutNormal);
null, timeoutNormal);
result.addErrorStatus(410);// gone
result.addErrorStatus(404);// unknown
result.addErrorStatus(405);
@ -835,11 +555,11 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
Parameters params = new Parameters();
params.addParameter().setName("name").setValue(new StringType(name));
params.addParameter().setName("concept").setValue(coding);
List<Header> headers = null;
ResourceRequest<Resource> result = utils.issuePostRequest(
resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
utils.getResourceAsByteArray(params, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"),
headers, timeoutOperation);
null, timeoutOperation);
result.addErrorStatus(410);// gone
result.addErrorStatus(404);// unknown
result.addErrorStatus(405);
@ -861,22 +581,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
utils.setTimeout(timeout);
}
public String getUsername() {
return utils.getUsername();
}
public void setUsername(String username) {
utils.setUsername(username);
}
public String getPassword() {
return utils.getPassword();
}
public void setPassword(String password) {
utils.setPassword(password);
}
public Parameters getTerminologyCapabilities() {
return (Parameters) utils
.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), withVer(getPreferredResourceFormat(), "1.0"), timeoutNormal)

View File

@ -233,9 +233,8 @@ 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
*
* @param locationResponseHeader
*/
public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);

View File

@ -32,11 +32,15 @@ package org.hl7.fhir.dstu2.utils.client;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import org.hl7.fhir.dstu2.model.Resource;
public class ResourceRequest<T extends Resource> {
@Getter
private T payload;
@Getter
private int httpStatus = -1;
@Getter
private String location;
private List<Integer> successfulStatuses = new ArrayList<Integer>();
private List<Integer> errorStatuses = new ArrayList<Integer>();
@ -67,14 +71,6 @@ public class ResourceRequest<T extends Resource> {
this.location = location;
}
public int getHttpStatus() {
return httpStatus;
}
public T getPayload() {
return payload;
}
public T getReference() {
T payloadResource = null;
if (payload != null) {
@ -99,7 +95,4 @@ public class ResourceRequest<T extends Resource> {
this.errorStatuses.add(status);
}
public String getLocation() {
return location;
}
}

View File

@ -0,0 +1,350 @@
package org.hl7.fhir.dstu2.utils.client;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.hl7.fhir.dstu2.formats.JsonParser;
import org.hl7.fhir.dstu2.model.*;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPHeaderUtil;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class ClientUtilsTest {
public static final String DUMMY_LOCATION = "http://myhost/Patient/1";
public static final int TIMEOUT = 5000;
private MockWebServer server;
private HttpUrl serverUrl;
private ClientUtils clientUtils;
private final Address address = new Address()
.setCity("Toronto")
.setState("Ontario")
.setCountry("Canada");
private final HumanName humanName = new HumanName()
.addGiven("Mark")
.addFamily("Iantorno");
private final Patient patient = new Patient()
.addName(humanName)
.addAddress(address)
.setGender(Enumerations.AdministrativeGender.MALE);
@BeforeEach
void setup() {
setupMockServer();
clientUtils = new ClientUtils();
}
void setupMockServer() {
server = new MockWebServer();
serverUrl = server.url("/v1/endpoint");
}
@Test
@DisplayName("Test resource format headers are added correctly.")
void addResourceFormatHeadersGET() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET);
Iterable<HTTPHeader> headers = ClientUtils.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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.assertNull(headersMap.get("Content-Type"), "Content-Type header null.");
}
@Test
@DisplayName("Test resource format headers are added correctly (POST).")
void addResourceFormatHeadersPOST() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST);
Iterable<HTTPHeader> headers = ClientUtils.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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=" + ClientUtils.DEFAULT_CHARSET, headersMap.get("Content-Type").get(0),
"Content-Type header not populated with expected value \"" + testFormat + ";charset=" + ClientUtils.DEFAULT_CHARSET + "\".");
}
@Test
public void testResourceFormatHeaders_GET() throws IOException, InterruptedException {
server.enqueue(
new MockResponse()
.setBody(new String(generateResourceBytes(patient)))
.addHeader("Content-Type", ResourceFormat.RESOURCE_JSON)
);
ResourceRequest<Patient> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT);
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getHeader("Accept")).isEqualTo("application/json+fhir");
}
@Test
public void testResourceFormatHeaders_POST() throws IOException, InterruptedException {
byte[] payload = generateResourceBytes(patient);
server.enqueue(
new MockResponse()
.setBody(new String(payload))
.addHeader("Content-Type", "application/json+fhir")
.setResponseCode(201)
);
ResourceRequest<Patient> resourceRequest = clientUtils.issuePostRequest(serverUrl.uri(), payload, "application/json+fhir", TIMEOUT);
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getHeader("Accept")).isEqualTo("application/json+fhir");
}
@Test
public void testResourceRequest() {
ResourceRequest<Patient> request = new ResourceRequest<>(new Patient(), 200, "location");
request.addSuccessStatus(200);
assertTrue(request.getPayload().equalsDeep(new Patient()));
assertThat(request.getHttpStatus()).isEqualTo(200);
assertThat(request.getLocation()).isEqualTo("location");
assertTrue(request.isSuccessfulRequest());
assertFalse(request.isUnsuccessfulRequest());
}
@Test
public void testIssueGetResourceRequest() throws IOException {
server.enqueue(
new MockResponse()
.setBody(new String(generateResourceBytes(patient)))
.addHeader("Content-Type", "application/json+fhir")
.addHeader("Location", DUMMY_LOCATION)
);
ResourceRequest<Patient> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT);
resourceRequest.addSuccessStatus(200);
assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION);
assertTrue(resourceRequest.isSuccessfulRequest());
assertTrue(patient.equalsDeep(resourceRequest.getPayload()));
}
@Test
void testIssueGetResourceRequest_withContentLocation() throws IOException {
server.enqueue(
new MockResponse()
.setBody(new String(generateResourceBytes(patient)))
.addHeader("Content-Type", "application/json+fhir")
.addHeader("Content-Location", DUMMY_LOCATION)
);
ResourceRequest<Patient> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT);
assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION);
}
@Test
@DisplayName("Test that getLocationHeader returns 'location' header when both 'location' and 'content-location' are set.")
void testIssueGetResourceRequest_ReturnsLocationHeaderWhenBothSet() throws IOException {
server.enqueue(
new MockResponse()
.setBody(new String(generateResourceBytes(patient)))
.addHeader("Content-Type", "application/json+fhir")
.addHeader("Location", DUMMY_LOCATION)
.addHeader("Content-Location", "Wrong wrong wrong")
);
ResourceRequest<Patient> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT);
assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION);
}
@Test
@DisplayName("Test that getLocationHeader returns 'null' header when neither 'location' or 'content-location' are set.")
void testIssueGetResourceRequest_ReturnsNullWhenNoHeadersSet() throws IOException {
server.enqueue(
new MockResponse()
.setBody(new String(generateResourceBytes(patient)))
.addHeader("Content-Type", "application/json+fhir")
);
ResourceRequest<Patient> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT);
assertThat(resourceRequest.getLocation()).isNull();
}
@Test
public void testIssuePostRequest() throws IOException, InterruptedException {
byte[] payload = generateResourceBytes(patient);
server.enqueue(
new MockResponse()
.setBody(new String(payload))
.addHeader("Content-Type", "application/json+fhir")
.addHeader("Location", DUMMY_LOCATION)
.setResponseCode(201)
);
ResourceRequest<Patient> resourceRequest = clientUtils.issuePostRequest(serverUrl.uri(), payload, "application/json+fhir", TIMEOUT);
resourceRequest.addSuccessStatus(201);
RecordedRequest recordedRequest = server.takeRequest();
assertArrayEquals(payload, recordedRequest.getBody().readByteArray(),
"PUT request payload does not match send data.");
assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION);
assertTrue(resourceRequest.isSuccessfulRequest());
assertTrue(patient.equalsDeep(resourceRequest.getPayload()));
}
@Test
public void testIssuePutRequest() throws IOException, InterruptedException {
byte[] payload = generateResourceBytes(patient);
server.enqueue(
new MockResponse()
.setBody(new String(payload))
.addHeader("Content-Type", "application/json+fhir")
.addHeader("Location", DUMMY_LOCATION)
.setResponseCode(200)
);
ResourceRequest<Patient> resourceRequest = clientUtils.issuePutRequest(serverUrl.uri(), payload, "application/json+fhir", TIMEOUT);
resourceRequest.addSuccessStatus(200);
RecordedRequest recordedRequest = server.takeRequest();
assertArrayEquals(payload, recordedRequest.getBody().readByteArray(),
"PUT request payload does not match send data.");
assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION);
assertTrue(resourceRequest.isSuccessfulRequest());
assertTrue(patient.equalsDeep(resourceRequest.getPayload()));
}
@Test
public void testIssueDeleteRequest() throws IOException {
server.enqueue(
new MockResponse().addHeader("Location", DUMMY_LOCATION).setResponseCode(204)
);
boolean success = clientUtils.issueDeleteRequest(serverUrl.uri());
assertTrue(success);
}
@Test
public void testIssueDeleteRequest_fail() throws IOException, InterruptedException {
server.enqueue(
new MockResponse().addHeader("Location", DUMMY_LOCATION).setResponseCode(500)
);
boolean success = clientUtils.issueDeleteRequest(serverUrl.uri());
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo("DELETE");
assertThat(recordedRequest.getRequestUrl().uri()).isEqualTo(serverUrl.uri());
assertFalse(success);
}
@Test
@DisplayName("test logger works")
public void testLogger() throws IOException, InterruptedException {
byte[] payload = generateResourceBytes(patient);
server.enqueue(
new MockResponse()
.setResponseCode(200)
.setBody(new String(payload))
);
ToolingClientLogger mockLogger = Mockito.mock(ToolingClientLogger.class);
clientUtils.setLogger(mockLogger);
clientUtils.issuePostRequest(serverUrl.uri(), payload, "application/json+fhir"
, null, TIMEOUT);
server.takeRequest();
Mockito.verify(mockLogger, Mockito.times(1))
.logRequest(Mockito.anyString(), Mockito.anyString(), Mockito.anyList(), Mockito.any());
Mockito.verify(mockLogger, Mockito.times(1))
.logResponse(Mockito.anyString(), Mockito.anyList(), Mockito.any(), Mockito.anyLong());
}
@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(clientUtils.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(clientUtils.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(clientUtils.hasError(outcome), "Error check triggered unexpectedly.");
}
@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))));
clientUtils.setRetryCount(failedAttempts + 1);
ResourceRequest<Resource> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir"
, TIMEOUT);
resourceRequest.addSuccessStatus(200);
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))));
clientUtils.setRetryCount(failedAttempts + 1);
ResourceRequest<Resource> resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT);
resourceRequest.addSuccessStatus(200);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");
}
byte[] generateResourceBytes(Resource resource) throws IOException {
return new JsonParser().composeBytes(resource);
}
}

View File

@ -0,0 +1,166 @@
package org.hl7.fhir.dstu2.utils.client;
import org.hl7.fhir.dstu2.model.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import java.net.URI;
import java.net.URISyntaxException;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.*;
public class FHIRToolingClientTest {
private final Address address = new Address()
.setCity("Toronto")
.setState("Ontario")
.setCountry("Canada");
private final HumanName humanName = new HumanName()
.addGiven("Mark")
.addFamily("Iantorno");
private final Patient patient = new Patient()
.addName(humanName)
.addAddress(address)
.setGender(Enumerations.AdministrativeGender.MALE);
private ClientUtils mockClientUtils;
private FHIRToolingClient toolingClient;
@Captor
private ArgumentCaptor<URI> uriArgumentCaptor;
@BeforeEach
public void beforeEach() throws URISyntaxException {
MockitoAnnotations.openMocks(this);
mockClientUtils = Mockito.mock(ClientUtils.class);
toolingClient = new FHIRToolingClient("http://dummy-base-url.com", "dummy-user-agent") {
@Override
protected ClientUtils getClientUtils() {
return mockClientUtils;
}
};
/*
Need to reset here. When initialized, the client makes a call to getConformanceStatementQuick, which messes with
our expected calls.
*/
reset(mockClientUtils);
}
@Test
public void testGetTerminologyCapabilities() throws URISyntaxException {
Parameters expectedCapabilities = new Parameters();
expectedCapabilities.addParameter().setName("name").setValue(new StringType("dummyValue"));
when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(expectedCapabilities, 200, "location"));
Parameters actualCapabilities = toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt());
assertThat(actualCapabilities).isEqualTo(expectedCapabilities);
assertThat(uriArgumentCaptor.getValue()).isEqualTo(new URI("http://dummy-base-url.com/metadata?mode=terminology"));
}
@Test
public void testGetConformanceStatement() throws URISyntaxException {
Conformance expectedConformance = new Conformance();
expectedConformance.setCopyright("dummyCopyright");
when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(expectedConformance, 200, "location"));
Conformance actualConformance = toolingClient.getConformanceStatement();
Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt());
assertThat(actualConformance).isEqualTo(expectedConformance);
assertThat(uriArgumentCaptor.getValue()).isEqualTo(new URI("http://dummy-base-url.com/metadata"));
}
@Test
public void testGetConformanceStatementQuick() throws URISyntaxException {
Conformance expectedConformance = new Conformance();
expectedConformance.setCopyright("dummyCopyright");
when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(expectedConformance, 200, "location"));
Conformance actualConformance = toolingClient.getConformanceStatementQuick();
Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt());
assertThat(actualConformance).isEqualTo(expectedConformance);
assertThat(uriArgumentCaptor.getValue()).isEqualTo(new URI("http://dummy-base-url.com/metadata?_summary=true"));
}
@Test
void testRead() {
when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(patient, 200, "location"));
Patient actualPatient = toolingClient.read(Patient.class, "id");
Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt());
assertThat(actualPatient).isEqualTo(patient);
assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/id");
}
@Test
void testVRead() {
when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(patient, 200, "location"));
Patient actualPatient = toolingClient.vread(Patient.class, "id", "version");
Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt());
assertThat(actualPatient).isEqualTo(patient);
assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/id/_history/version");
}
@Test
void testCanonical() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(patient);
when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(bundle, 200, "location"));
Patient actualPatient = toolingClient.getCanonical(Patient.class, "canonicalURL");
Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt());
assertThat(actualPatient).isEqualTo(patient);
assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient?url=canonicalURL");
}
@Test
void testUpdate() {
final byte[] dummyBytes = "dummyBytes".getBytes();
when(mockClientUtils.getResourceAsByteArray(any(Patient.class), anyBoolean(), anyBoolean())).thenReturn(dummyBytes);
when(mockClientUtils.issuePutRequest(uriArgumentCaptor.capture(), Mockito.any(byte[].class), Mockito.anyString(), Mockito.isNull(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(patient, 200, "location"));
Patient actualPatient = toolingClient.update(Patient.class, patient, "id");
Mockito.verify(mockClientUtils).issuePutRequest(ArgumentMatchers.any(URI.class), Mockito.any(byte[].class), ArgumentMatchers.anyString(), ArgumentMatchers.isNull(),ArgumentMatchers.anyInt());
assertThat(actualPatient).isEqualTo(patient);
assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/id");
}
@Test
void testValidate() {
final byte[] dummyBytes = "dummyBytes".getBytes();
final OperationOutcome expectedOutcome = new OperationOutcome();
OperationOutcome.OperationOutcomeIssueComponent issueComponent = expectedOutcome.addIssue();
issueComponent.setSeverity(OperationOutcome.IssueSeverity.ERROR);
when(mockClientUtils.getResourceAsByteArray(any(Patient.class), anyBoolean(), anyBoolean())).thenReturn(dummyBytes);
when(mockClientUtils.issuePostRequest(uriArgumentCaptor.capture(), Mockito.any(byte[].class), Mockito.anyString(), Mockito.anyInt()))
.thenReturn(new ResourceRequest<>(expectedOutcome, 200, "location"));
OperationOutcome actualOutcome = toolingClient.validate(Patient.class, patient, "id");
assertThat(actualOutcome).isEqualTo(expectedOutcome);
assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/$validate/id");
}
}

View File

@ -28,6 +28,12 @@
<artifactId>hapi-fhir-base</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- UCUM -->
<dependency>
<groupId>org.fhir</groupId>
@ -99,6 +105,13 @@
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>

View File

@ -3,18 +3,15 @@ package org.hl7.fhir.dstu3.utils.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.CapabilityStatement;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.ConceptMap;
import org.hl7.fhir.dstu3.model.ExpansionProfile;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
@ -31,8 +28,7 @@ import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
import okhttp3.Headers;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.utilities.http.HTTPHeader;
/**
* Very Simple RESTful client. This is purely for use in the standalone
@ -72,13 +68,17 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
private int maxResultSetSize = -1;//_count
private CapabilityStatement capabilities;
private Client client = new Client();
private ArrayList<Header> headers = new ArrayList<>();
private String username;
private String password;
private List<HTTPHeader> headers = new ArrayList<>();
@Setter
@Getter
private String userAgent;
private EnumSet<FhirPublication> allowedVersions;
private String acceptLang;
private String contentLang;
@Setter
@Getter
private String acceptLanguage;
@Setter
private String contentLanguage;
@Getter
private int useCount;
//Pass endpoint for client - URI
@ -150,7 +150,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
capabilities = (Parameters) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"TerminologyCapabilities",
timeoutNormal).getReference();
} catch (Exception e) {
@ -164,7 +164,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"CapabilitiesStatement",
timeoutNormal).getReference();
} catch (Exception e) {
@ -178,7 +178,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"CapabilitiesStatement-Quick",
timeoutNormal).getReference();
} catch (Exception e) {
@ -193,7 +193,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"Read " + resourceClass.getName() + "/" + id,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
@ -211,7 +211,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
@ -229,7 +229,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"Read " + resourceClass.getName() + "?url=" + canonicalURL,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
@ -253,7 +253,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"Update " + resource.fhirType() + "/" + resource.getId(),
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
@ -281,7 +281,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"Update " + resource.fhirType() + "/" + id,
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
@ -320,13 +320,13 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
if (client.getLogger() != null) {
client.getLogger().logRequest("POST", url.toString(), null, body);
}
result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(),
result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(true),
"POST " + resourceClass.getName() + "/$" + name, timeoutLong);
} else {
if (client.getLogger() != null) {
client.getLogger().logRequest("GET", url.toString(), null, null);
}
result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, timeoutLong);
result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(false), "GET " + resourceClass.getName() + "/$" + name, timeoutLong);
}
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
@ -363,7 +363,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(),
withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(true),
"POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
@ -375,10 +375,14 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
}
/**
* Helper method to prevent nesting of previously thrown EFhirClientExceptions
* Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of
* EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the
* cause.
*
* @param e
* @throws EFhirClientException
* @param message The EFhirClientException message.
* @param e The exception
* @throws EFhirClientException EFhirClientException representing the exception.
*/
protected void handleException(String message, Exception e) throws EFhirClientException {
if (e instanceof EFhirClientException) {
@ -389,11 +393,11 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
}
/**
* Helper method to determine whether desired resource representation
* is Json or XML.
* Helper method to determine whether desired resource representation is Json or
* XML.
*
* @param format
* @return
* @param format the format
* @return true if the format is JSON, false otherwise
*/
protected boolean isJson(String format) {
boolean isJson = false;
@ -420,7 +424,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(false),
"CodeSystem/$lookup",
timeoutNormal);
} catch (IOException e) {
@ -439,7 +443,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"CodeSystem/$lookup",
timeoutNormal);
} catch (IOException e) {
@ -458,7 +462,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(ConceptMap.class, "transform"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"ConceptMap/$transform",
timeoutNormal);
} catch (IOException e) {
@ -479,7 +483,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"ValueSet/$expand?url=" + source.getUrl(),
timeoutExpand);
if (result.isUnsuccessfulRequest()) {
@ -504,7 +508,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"Closure?name=" + name,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
@ -526,7 +530,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
generateHeaders(),
generateHeaders(true),
"UpdateClosure?name=" + name,
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
@ -538,21 +542,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return result == null ? null : (ConceptMap) result.getPayload();
}
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 long getTimeout() {
return client.getTimeout();
@ -578,48 +567,27 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
client.setRetryCount(retryCount);
}
public void setClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
public void setClientHeaders(Iterable<HTTPHeader> headers) {
this.headers =new ArrayList<>();
headers.forEach(this.headers::add);
}
private Headers generateHeaders() {
Headers.Builder builder = new Headers.Builder();
// Add basic auth header if it exists
if (basicAuthHeaderExists()) {
builder.add(getAuthorizationHeader().toString());
}
private Iterable<HTTPHeader> generateHeaders(boolean hasBody) {
// Add any other headers
if(this.headers != null) {
this.headers.forEach(header -> builder.add(header.toString()));
}
List<HTTPHeader> headers = new ArrayList<>(this.headers);
if (!Utilities.noString(userAgent)) {
builder.add("User-Agent: "+userAgent);
}
if (!Utilities.noString(acceptLang)) {
builder.add("Accept-Language: "+acceptLang);
headers.add(new HTTPHeader("User-Agent",userAgent));
}
if (!Utilities.noString(contentLang)) {
builder.add("Content-Language: "+contentLang);
if (!Utilities.noString(acceptLanguage)) {
headers.add(new HTTPHeader("Accept-Language", acceptLanguage));
}
return builder.build();
}
public boolean basicAuthHeaderExists() {
return (username != null) && (password != null);
}
if (hasBody && !Utilities.noString(contentLanguage)) {
headers.add(new HTTPHeader("Content-Language",contentLanguage));
}
public Header getAuthorizationHeader() {
String usernamePassword = username + ":" + password;
String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
return new Header("Authorization", "Basic " + base64usernamePassword);
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
return headers;
}
public String getServerVersion() {
@ -627,17 +595,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return capabilities == null ? null : capabilities.getSoftware().getVersion();
}
public void setAcceptLanguage(String lang) {
this.acceptLang = lang;
}
public void setContentLanguage(String lang) {
this.contentLang = lang;
}
public int getUseCount() {
return useCount;
}
private void recordUse() {
useCount++;
}

View File

@ -2,6 +2,7 @@ package org.hl7.fhir.dstu3.utils.client.network;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -10,10 +11,8 @@ import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.ToolingClientLogger;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPRequest;
public class Client {
@ -61,21 +60,22 @@ public class Client {
String resourceFormat,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.method("OPTIONS", null)
.url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(optionsUri.toURL())
.withMethod(HTTPRequest.HttpMethod.OPTIONS);
return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> headers,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
@ -87,21 +87,21 @@ public class Client {
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> 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);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.PUT)
.withBody(payload);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
@ -110,36 +110,36 @@ public class Client {
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
return issuePostRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> 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);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload);
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, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest();
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.DELETE);
return executeFhirRequest(request, null, Collections.emptyList(), 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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle issuePostFeedRequest(URI resourceUri,
@ -149,12 +149,13 @@ public class Client {
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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload);
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
@ -163,17 +164,17 @@ public class Client {
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, new Headers.Builder().build(), message, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload);
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
public <T extends Resource> Bundle executeBundleRequest(HTTPRequest request,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> headers,
String message,
int retryCount,
long timeout) throws IOException {
@ -182,23 +183,23 @@ public class Client {
.withResourceFormat(resourceFormat)
.withRetryCount(retryCount)
.withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS)
.executeAsBatch();
}
public <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
public <T extends Resource> ResourceRequest<T> executeFhirRequest(HTTPRequest request,
String resourceFormat,
Iterable<HTTPHeader> headers,
String message,
int retryCount,
long timeout) throws IOException {
return new FhirRequestBuilder(request, base)
.withLogger(logger)
.withResourceFormat(resourceFormat)
.withRetryCount(retryCount)
.withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS)
.execute();
}

View File

@ -5,8 +5,7 @@ import java.util.List;
import java.util.stream.Collectors;
import org.hl7.fhir.exceptions.FHIRException;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.utilities.http.HTTPHeader;
/**
* Generic Implementation of Client Headers.
@ -16,30 +15,30 @@ import okhttp3.internal.http2.Header;
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
private final ArrayList<HTTPHeader> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
public ClientHeaders(ArrayList<HTTPHeader> headers) {
this.headers = headers;
}
public ArrayList<Header> headers() {
public ArrayList<HTTPHeader> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @param header {@link HTTPHeader} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
public ClientHeaders addHeader(HTTPHeader header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", "
+ header.value + ">.");
throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", "
+ header.getValue() + ">.");
}
headers.add(header);
return this;
@ -48,39 +47,39 @@ public class ClientHeaders {
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @param headerList {@link List} of {@link HTTPHeader} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders addHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
/**
* Removes the passed in header from the list of stored headers.
* @param header {@link Header} to remove from the list.
* @param header {@link HTTPHeader} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", "
+ header.value + ">, from GenericClientHeaders that is not currently stored.");
throw new FHIRException("Attempting to remove header, <" + header.getName() + ", "
+ header.getValue() + ">, from GenericClientHeaders that is not currently stored.");
}
return this;
}
/**
* Removes the passed in headers from the list of stored headers.
* @param headerList {@link List} of {@link Header} to remove.
* @param headerList {@link List} of {@link HTTPHeader} to remove.
* @throws FHIRException if any of the headers passed in does not exist within the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders removeHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
* Clears all stored {@link HTTPHeader}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
@ -90,7 +89,7 @@ public class ClientHeaders {
@Override
public String toString() {
return this.headers.stream()
.map(header -> "\t" + header.name + ":" + header.value)
.map(header -> "\t" + header.getName() + ":" + header.getValue())
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -2,12 +2,11 @@ package org.hl7.fhir.dstu3.utils.client.network;
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;
import javax.annotation.Nonnull;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.formats.IParser;
@ -19,33 +18,20 @@ import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.utils.ResourceUtilities;
import org.hl7.fhir.dstu3.utils.client.EFhirClientException;
import org.hl7.fhir.dstu3.utils.client.ResourceFormat;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.MimeType;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.utilities.http.*;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
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 final HTTPRequest httpRequest;
private String resourceFormat = null;
private Headers headers = null;
private Iterable<HTTPHeader> headers = null;
private String message = null;
private int retryCount = 1;
/**
@ -59,60 +45,41 @@ public class FhirRequestBuilder {
/**
* {@link ToolingClientLogger} for log output.
*/
@Getter @Setter
private ToolingClientLogger logger = null;
private String source;
public FhirRequestBuilder(Request.Builder httpRequest, String source) {
public FhirRequestBuilder(HTTPRequest httpRequest, String source) {
this.httpRequest = httpRequest;
this.source = source;
}
/**
* Adds necessary default headers, formatting headers, and any passed in {@link Headers} to the passed in
* {@link okhttp3.Request.Builder}
* Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s to the passed in
* {@link HTTPRequest}
*
* @param request {@link okhttp3.Request.Builder} to add headers to.
* @param request {@link HTTPRequest} to add headers to.
* @param format Expected {@link Resource} format.
* @param headers Any additional {@link Headers} to add to the request.
* @param headers Any additional {@link HTTPHeader}s to add to the request.
*/
protected static void formatHeaders(Request.Builder request, String format, Headers headers) {
addDefaultHeaders(request, headers);
if (format != null) addResourceFormatHeaders(request, format);
if (headers != null) addHeaders(request, headers);
}
protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable<HTTPHeader> headers) {
List<HTTPHeader> allHeaders = new ArrayList<>();
request.getHeaders().forEach(allHeaders::add);
/**
* Adds necessary headers for all REST requests.
* <li>User-Agent : hapi-fhir-tooling-client</li>
*
* @param request {@link Request.Builder} to add default headers to.
*/
protected static void addDefaultHeaders(Request.Builder request, Headers headers) {
if (headers == null || !headers.names().contains("User-Agent")) {
request.addHeader("User-Agent", "hapi-fhir-tooling-client");
}
}
/**
* 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) {
if (headers != null) {
headers.forEach(header -> request.addHeader(header.getFirst(), header.getSecond()));
if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add);
if (headers != null) headers.forEach(allHeaders::add);
return request.withHeaders(allHeaders);
}
protected static Iterable<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
List<HTTPHeader> headers = new ArrayList<>();
headers.add(new HTTPHeader("Accept", format));
if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
) {
headers.add(new HTTPHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET));
}
return headers;
}
/**
@ -131,69 +98,24 @@ public class FhirRequestBuilder {
}
/**
* 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.
* Extracts the 'location' header from the passed {@link Iterable<HTTPHeader>}. 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
* @param headers {@link HTTPHeader} 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;
protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
if (locationHeader != null) {
return locationHeader;
}
return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
}
/**
* 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 (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
Authenticator proxyAuthenticator = getAuthenticator();
return okHttpClient.newBuilder()
.addInterceptor(new RetryInterceptor(retryCount))
.connectTimeout(timeout, timeoutUnit)
.writeTimeout(timeout, timeoutUnit)
.readTimeout(timeout, timeoutUnit)
.proxyAuthenticator(proxyAuthenticator)
.build();
}
@Nonnull
private static Authenticator getAuthenticator() {
return (route, response) -> {
final String httpProxyUser = System.getProperty(HTTP_PROXY_USER);
final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS);
if (httpProxyUser != null && httpProxyPass != null) {
String credential = Credentials.basic(httpProxyUser, httpProxyPass);
return response.request().newBuilder()
.header(HEADER_PROXY_AUTH, credential)
.build();
}
return response.request().newBuilder().build();
};
protected ManagedFhirWebAccessor getManagedWebAccessor() {
return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
}
public FhirRequestBuilder withResourceFormat(String resourceFormat) {
@ -201,7 +123,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withHeaders(Headers headers) {
public FhirRequestBuilder withHeaders(Iterable<HTTPHeader> headers) {
this.headers = headers;
return this;
}
@ -227,25 +149,16 @@ public class FhirRequestBuilder {
return this;
}
protected Request buildRequest() {
return httpRequest.build();
}
public <T extends Resource> ResourceRequest<T> execute() throws IOException {
formatHeaders(httpRequest, resourceFormat, headers);
final Request request = httpRequest.build();
log(request.method(), request.url().toString(), request.headers(), request.body() != null ? request.body().toString().getBytes() : null);
Response response = getHttpClient().newCall(request).execute();
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
T resource = unmarshalReference(response, resourceFormat);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
final Request request = httpRequest.build();
log(request.method(), request.url().toString(), request.headers(), request.body() != null ? request.body().toString().getBytes() : null);
Response response = getHttpClient().newCall(request).execute();
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
return unmarshalFeed(response, resourceFormat);
}
@ -253,14 +166,14 @@ public class FhirRequestBuilder {
* Unmarshalls a resource from the response stream.
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(Response response, String format) {
protected <T extends Resource> T unmarshalReference(HTTPResult response, String format) {
T resource = null;
OperationOutcome error = null;
if (response.body() != null) {
if (response.getContent() != null) {
try {
byte[] body = response.body().bytes();
log(response.code(), response.headers(), body);
byte[] body = response.getContent();
resource = (T) getParser(format).parse(body);
if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) {
error = (OperationOutcome) resource;
@ -282,13 +195,13 @@ public class FhirRequestBuilder {
/**
* Unmarshalls Bundle from response stream.
*/
protected Bundle unmarshalFeed(Response response, String format) {
protected Bundle unmarshalFeed(HTTPResult 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");
byte[] body = response.getContent();
String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
if (body != null) {
if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains(ResourceFormat.RESOURCE_JSON.getHeader()) || contentType.contains("text/xml+fhir")) {
Resource rf = getParser(format).parse(body);
@ -315,7 +228,7 @@ public class FhirRequestBuilder {
/**
* Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is
* provided...because reasons.
* <p>
* <p/>
* Currently supports only "json" and "xml" formats.
*
* @param format One of "json" or "xml".
@ -334,52 +247,4 @@ public class FhirRequestBuilder {
throw new EFhirClientException("Invalid format: " + format);
}
}
/**
* Logs the given {@link Request}, using the current {@link ToolingClientLogger}. If the current
* {@link FhirRequestBuilder#logger} is null, no action is taken.
*
* @param method HTTP request method
* @param url request URL
* @param requestHeaders {@link Headers} for request
* @param requestBody Byte array request
*/
protected void log(String method, String url, Headers requestHeaders, byte[] requestBody) {
if (logger != null) {
List<String> headerList = new ArrayList<>(Collections.emptyList());
Map<String, List<String>> headerMap = requestHeaders.toMultimap();
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
logger.logRequest(method, url, headerList, requestBody);
}
}
/**
* 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 {
if (logger != null) {
logger.logResponse(Integer.toString(responseCode), headerList, responseBody, 0);
}
} 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,225 @@
package org.hl7.fhir.dstu3.utils.client;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.utils.client.network.Client;
import org.hl7.fhir.dstu3.utils.client.network.ResourceRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FhirToolingClientTest {
String TX_ADDR = "http://tx.fhir.org";
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases");
private Client mockClient;
private FHIRToolingClient toolingClient;
@Captor
private ArgumentCaptor<Iterable<HTTPHeader>> headersArgumentCaptor;
@BeforeEach
void setUp() throws IOException, URISyntaxException {
MockitoAnnotations.openMocks(this);
mockClient = Mockito.mock(Client.class);
ResourceRequest<Resource> resourceResourceRequest = new ResourceRequest<>(generateBundle(), 200, "");
// GET
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new Parameters(), 200, "location"));
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
// PUT
Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
// POST
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
Mockito
.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location"));
// BUNDLE REQ
Mockito
.when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
.thenReturn(generateBundle());
toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases");
toolingClient.setClient(mockClient);
}
private List<HTTPHeader> getHeaders() {
return new ArrayList<>(Arrays.asList(h1, h2, h3));
}
private List<HTTPHeader> getHeadersWithAgent() {
return new ArrayList<>(Arrays.asList(h1, h2, h3, agentHeader));
}
private Bundle generateBundle() {
Patient patient = generatePatient();
Observation observation = generateObservation();
// The observation refers to the patient using the ID, which is already
// set to a temporary UUID
observation.setSubject(new Reference(patient.getIdElement().getValue()));
// Create a bundle that will be used as a transaction
Bundle bundle = new Bundle();
// Add the patient as an entry.
bundle.addEntry().setFullUrl(patient.getIdElement().getValue()).setResource(patient).getRequest().setUrl("Patient")
.setIfNoneExist("identifier=http://acme.org/mrns|12345").setMethod(Bundle.HTTPVerb.POST);
return bundle;
}
private Patient generatePatient() {
// Create a patient object
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://acme.org/mrns").setValue("12345");
patient.addName().setFamily("Jameson").addGiven("J").addGiven("Jonah");
patient.setGender(Enumerations.AdministrativeGender.MALE);
// Give the patient a temporary UUID so that other resources in
// the transaction can refer to it
patient.setId(IdType.newRandomUuid());
return patient;
}
private Observation generateObservation() {
// Create an observation object
Observation observation = new Observation();
observation.getCode().addCoding().setSystem("http://loinc.org").setCode("789-8")
.setDisplay("Erythrocytes [#/volume] in Blood by Automated count");
observation.setValue(new Quantity().setValue(4.12).setUnit("10 trillion/L").setSystem("http://unitsofmeasure.org")
.setCode("10*12/L"));
return observation;
}
private void checkHeaders(Iterable<HTTPHeader> argumentCaptorValue) {
List<HTTPHeader> capturedHeaders = new ArrayList<>();
argumentCaptorValue.forEach(capturedHeaders::add);
getHeadersWithAgent().forEach(header -> {
assertTrue(capturedHeaders.contains(header));
});
}
@Test
void getTerminologyCapabilities() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatement() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatement();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatementQuick() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatementQuick();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void read() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.read(Patient.class, "id");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void vread() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.vread(Patient.class, "id", "version");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCanonical() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCanonical(Patient.class, "canonicalURL");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void update() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.update(generatePatient());
Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void validate() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.validate(Patient.class, generatePatient(), "id");
Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
}

View File

@ -0,0 +1,169 @@
package org.hl7.fhir.dstu3.utils.client.network;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.hl7.fhir.dstu3.formats.JsonParser;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class ClientTest {
private static final long TIMEOUT = 5000;
private MockWebServer server;
private HttpUrl serverUrl;
private Client client;
private final Address address = new Address()
.setCity("Toronto")
.setState("Ontario")
.setCountry("Canada");
private final HumanName humanName = new HumanName()
.addGiven("Mark")
.setFamily("Iantorno");
private final 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, 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, 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, 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, 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, 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.");
}
@Test
@DisplayName("Testing the logger works.")
void test_logger() throws IOException, URISyntaxException, InterruptedException {
byte[] payload = ByteUtils.resourceToByteArray(patient, true, false, false);
server.enqueue(
new MockResponse()
.setResponseCode(200)
.setBody(new String(payload))
);
ToolingClientLogger mockLogger = Mockito.mock(ToolingClientLogger.class);
client.setLogger(mockLogger);
client.issuePostRequest(new URI(serverUrl.toString()), payload,
"xml", null, TIMEOUT);
server.takeRequest();
Mockito.verify(mockLogger, Mockito.times(1))
.logRequest(Mockito.anyString(), Mockito.anyString(), Mockito.anyList(), Mockito.any());
Mockito.verify(mockLogger, Mockito.times(1))
.logResponse(Mockito.anyString(), Mockito.anyList(), Mockito.any(), Mockito.anyLong());
}
}

View File

@ -1,107 +1,126 @@
package org.hl7.fhir.dstu3.utils.client.network;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.dstu3.formats.IParser;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.utilities.http.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
@ExtendWith(MockitoExtension.class)
public class FhirRequestBuilderTests {
private static final String DUMMY_URL = "https://some-url.com/";
@Test
@DisplayName("Test resource format headers are added correctly (GET).")
void addResourceFormatHeadersGET() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET);
Request mockRequest = new Request.Builder()
.url(DUMMY_URL)
.build();
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
final String RESPONSE_BODY_STRING = "{}";
Response response = new Response.Builder()
.request(mockRequest)
.protocol(Protocol.HTTP_2)
.code(200) // status code
.message("")
.body(ResponseBody.create(RESPONSE_BODY_STRING,
MediaType.get("application/json; charset=utf-8")
))
.addHeader("Content-Type", "")
.build();
final Request.Builder requestBuilder = new Request.Builder()
.url(DUMMY_URL);
final FhirRequestBuilder fhirRequestBuilder = Mockito.spy(new FhirRequestBuilder(requestBuilder, "http://local/local"));
@Mock
OkHttpClient client;
@Mock
Call mockCall;
@Mock
ToolingClientLogger logger;
@BeforeEach
public void beforeEach() {
Mockito.doReturn(client).when(fhirRequestBuilder).getHttpClient();
fhirRequestBuilder.withLogger(logger);
}
@Nested
class RequestLoggingTests {
@BeforeEach
public void beforeEach() throws IOException {
Mockito.doReturn(response).when(mockCall).execute();
Mockito.doReturn(mockCall).when(client).newCall(ArgumentMatchers.any());
Mockito.doReturn(null).when(fhirRequestBuilder).unmarshalReference(ArgumentMatchers.any(), ArgumentMatchers.isNull());
}
@Test
public void testExecuteLogging() throws IOException {
fhirRequestBuilder.execute();
Mockito.verify(logger).logRequest(ArgumentMatchers.eq("GET"), ArgumentMatchers.eq(DUMMY_URL), ArgumentMatchers.anyList(), ArgumentMatchers.isNull());
}
@Test
public void testExecuteBatchLogging() throws IOException {
fhirRequestBuilder.executeAsBatch();
Mockito.verify(logger).logRequest(ArgumentMatchers.eq("GET"), ArgumentMatchers.eq(DUMMY_URL), ArgumentMatchers.anyList(), ArgumentMatchers.isNull());
}
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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.assertNull(headersMap.get("Content-Type"), "Content-Type header not null.");
}
@Test
public void testUnmarshallReferenceLogging() {
IParser parser = Mockito.mock(IParser.class);
Mockito.doReturn(parser).when(fhirRequestBuilder).getParser(ArgumentMatchers.eq("json"));
@DisplayName("Test resource format headers are added correctly (POST).")
void addResourceFormatHeadersPOST() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST);
fhirRequestBuilder.unmarshalReference(response, "json");
Mockito.verify(logger).logResponse(ArgumentMatchers.eq("200"), ArgumentMatchers.anyList(), AdditionalMatchers.aryEq(RESPONSE_BODY_STRING.getBytes()), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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
public void testUnmarshallFeedLogging() {
fhirRequestBuilder.unmarshalFeed(response, "application/json");
Mockito.verify(logger).logResponse(ArgumentMatchers.eq("200"), ArgumentMatchers.anyList(), AdditionalMatchers.aryEq(RESPONSE_BODY_STRING.getBytes()), ArgumentMatchers.anyLong());
@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";
HTTPResult result = new HTTPResult("source",
200,
"message",
"contentType",
new byte[0],
List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader)));
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(result.getHeaders()));
}
@Test
@DisplayName("Test that getLocationHeader returns header for 'content-location'.")
void getLocationHeaderWhenOnlyContentLocationIsSet() {
final String expectedContentLocationHeader = "content_location_header_value";
Iterable<HTTPHeader> headers = List.of(new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader));
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";
Iterable<HTTPHeader> headers = List.of(
new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader),
new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)
);
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@Test
@DisplayName("Test that getLocationHeader returns null when no location available.")
void getLocationHeaderWhenNoLocationSet() {
Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList()));
}
}

View File

@ -35,6 +35,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- UCUM -->
<dependency>
<groupId>org.fhir</groupId>
@ -78,7 +84,12 @@
<artifactId>okio-jvm</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hl7.fhir.testcases</groupId>
<artifactId>fhir-test-cases</artifactId>

View File

@ -3,11 +3,10 @@ package org.hl7.fhir.r4.utils.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CapabilityStatement;
@ -29,8 +28,7 @@ import org.hl7.fhir.utilities.FHIRBaseToolingClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
import okhttp3.Headers;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.utilities.http.HTTPHeader;
/**
* Very Simple RESTful client. This is purely for use in the standalone tools
@ -70,16 +68,22 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
private String base;
private ResourceAddress resourceAddress;
@Setter
private ResourceFormat preferredResourceFormat;
private int maxResultSetSize = -1;// _count
private CapabilityStatement capabilities;
@Getter
@Setter
private Client client = new Client();
private ArrayList<Header> headers = new ArrayList<>();
private String username;
private String password;
private List<HTTPHeader> headers = new ArrayList<>();
@Getter @Setter
private String userAgent;
private String acceptLang;
private String contentLang;
@Setter
private String acceptLanguage;
@Setter
private String contentLanguage;
private int useCount;
// Pass endpoint for client - URI
@ -96,14 +100,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
this.maxResultSetSize = -1;
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
private void checkCapabilities() {
try {
capabilities = getCapabilitiesStatementQuick();
@ -115,10 +111,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return preferredResourceFormat.getHeader();
}
public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
preferredResourceFormat = resourceFormat;
}
public int getMaximumRecordCount() {
return maxResultSetSize;
}
@ -131,7 +123,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
TerminologyCapabilities capabilities = null;
try {
capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "TerminologyCapabilities", timeoutNormal).getReference();
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "TerminologyCapabilities", timeoutNormal).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's terminology capabilities", e);
}
@ -142,7 +134,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
CapabilityStatement conformance = null;
try {
conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CapabilitiesStatement", timeoutNormal).getReference();
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CapabilitiesStatement", timeoutNormal).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's conformance statement", e);
}
@ -154,7 +146,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return capabilities;
try {
capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CapabilitiesStatement-Quick", timeoutNormal)
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CapabilitiesStatement-Quick", timeoutNormal)
.getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's capability statement: " + e.getMessage(), e);
@ -167,7 +159,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
ResourceRequest<Resource> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Read " + resourceClass + "/" + id,
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "Read " + resourceClass + "/" + id,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -184,7 +176,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
ResourceRequest<T> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Read " + resourceClass.getName() + "/" + id,
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "Read " + resourceClass.getName() + "/" + id,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -202,7 +194,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issueGetResourceRequest(
resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false),
"VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -220,7 +212,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issueGetResourceRequest(
resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Read " + resourceClass.getName() + "?url=" + canonicalURL,
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "Read " + resourceClass.getName() + "?url=" + canonicalURL,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -244,7 +236,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePutRequest(
resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Update " + resource.fhirType() + "/" + resource.getId(),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Update " + resource.fhirType() + "/" + resource.getId(),
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -274,7 +266,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Update " + resource.fhirType() + "/" + id,
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Update " + resource.fhirType() + "/" + id,
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -312,10 +304,10 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
if (complex) {
byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true);
result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(),
result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true),
"POST " + resourceClass.getName() + "/$" + name, timeoutLong);
} else {
result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(),
result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false),
"GET " + resourceClass.getName() + "/$" + name, timeoutLong);
}
if (result.isUnsuccessfulRequest()) {
@ -351,7 +343,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true),
"POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -364,10 +356,14 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
}
/**
* Helper method to prevent nesting of previously thrown EFhirClientExceptions
* Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of
* EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the
* cause.
*
* @param e
* @throws EFhirClientException
* @param code The EFhirClientException code.
* @param message The EFhirClientException message.
* @param e The exception.
* @throws EFhirClientException representing the exception.
*/
protected void handleException(int code, String message, Exception e) throws EFhirClientException {
if (e instanceof EFhirClientException) {
@ -378,11 +374,11 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
}
/**
* Helper method to determine whether desired resource representation is Json or
* XML.
* Helper method to determine whether desired resource representation
* is Json or XML.
*
* @param format
* @return
* @param format The format
* @return true if the format is JSON, false otherwise
*/
protected boolean isJson(String format) {
boolean isJson = false;
@ -408,7 +404,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CodeSystem/$lookup", timeoutNormal);
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CodeSystem/$lookup", timeoutNormal);
} catch (IOException e) {
throw new FHIRException(e);
}
@ -425,7 +421,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CodeSystem/$lookup", timeoutNormal);
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "CodeSystem/$lookup", timeoutNormal);
} catch (IOException e) {
throw new FHIRException(e);
}
@ -442,7 +438,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(ConceptMap.class, "translate"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "ConceptMap/$translate", timeoutNormal);
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "ConceptMap/$translate", timeoutNormal);
} catch (IOException e) {
throw new FHIRException(e);
}
@ -463,7 +459,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
try {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), withVer(getPreferredResourceFormat(), "4.0"),
generateHeaders(), source == null ? "ValueSet/$expand" : "ValueSet/$expand?url=" + source.getUrl(),
generateHeaders(true), source == null ? "ValueSet/$expand" : "ValueSet/$expand?url=" + source.getUrl(),
timeoutExpand);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
@ -494,7 +490,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(
resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Closure?name=" + name, timeoutNormal);
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Closure?name=" + name, timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
@ -515,7 +511,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(
resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "UpdateClosure?name=" + name, timeoutOperation);
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "UpdateClosure?name=" + name, timeoutOperation);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
@ -526,22 +522,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return result == null ? null : (ConceptMap) result.getPayload();
}
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 client.getLogger();
}
@ -558,50 +538,27 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
client.setRetryCount(retryCount);
}
public void setClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
public void setClientHeaders(Iterable<HTTPHeader> headers) {
this.headers = new ArrayList<>();
headers.forEach(this.headers::add);
}
private Headers generateHeaders() {
Headers.Builder builder = new Headers.Builder();
// Add basic auth header if it exists
if (basicAuthHeaderExists()) {
builder.add(getAuthorizationHeader().toString());
}
private Iterable<HTTPHeader> generateHeaders(boolean hasBody) {
// Add any other headers
if (this.headers != null) {
this.headers.forEach(header -> builder.add(header.toString()));
}
List<HTTPHeader> headers = new ArrayList<>(this.headers);
if (!Utilities.noString(userAgent)) {
builder.add("User-Agent: " + userAgent);
headers.add(new HTTPHeader("User-Agent",userAgent));
}
if (!Utilities.noString(acceptLang)) {
builder.add("Accept-Language: "+acceptLang);
if (!Utilities.noString(acceptLanguage)) {
headers.add(new HTTPHeader("Accept-Language", acceptLanguage));
}
if (!Utilities.noString(contentLang)) {
builder.add("Content-Language: "+contentLang);
if (hasBody && !Utilities.noString(contentLanguage)) {
headers.add(new HTTPHeader("Content-Language",contentLanguage));
}
return builder.build();
}
public boolean basicAuthHeaderExists() {
return (username != null) && (password != null);
}
public Header getAuthorizationHeader() {
String usernamePassword = username + ":" + password;
String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
return new Header("Authorization", "Basic " + base64usernamePassword);
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
return headers;
}
public String getServerVersion() {
@ -609,14 +566,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return capabilities == null ? null : capabilities.getSoftware().getVersion();
}
public void setAcceptLanguage(String lang) {
this.acceptLang = lang;
}
public void setContentLanguage(String lang) {
this.acceptLang = lang;
}
public Bundle search(String type, String criteria) {
recordUse();
return fetchFeed(Utilities.pathURL(base, type+criteria));
@ -627,7 +576,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetResource(resourceClass, id),
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), resourceClass.getName()+"/"+id, timeoutNormal);
withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), resourceClass.getName()+"/"+id, timeoutNormal);
} catch (IOException e) {
throw new FHIRException(e);
}

View File

@ -2,148 +2,149 @@ package org.hl7.fhir.r4.utils.client.network;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.ToolingClientLogger;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPRequest;
public class Client {
public static final String DEFAULT_CHARSET = "UTF-8";
@Getter @Setter
private ToolingClientLogger logger;
private FhirLoggingInterceptor fhirLoggingInterceptor;
@Getter @Setter
private int retryCount;
@Getter @Setter
private String base;
public String getBase() {
return base;
}
public void setBase(String base) {
this.base = base;
}
public ToolingClientLogger getLogger() {
return logger;
}
public void setLogger(ToolingClientLogger logger) {
this.logger = logger;
this.fhirLoggingInterceptor = new FhirLoggingInterceptor(logger);
}
public int getRetryCount() {
return retryCount;
}
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
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, new Headers.Builder().build(), message, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(optionsUri.toURL())
.withMethod(HTTPRequest.HttpMethod.OPTIONS);
return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat,
Headers headers, String message, long timeout) throws IOException {
Request.Builder request = new Request.Builder().url(resourceUri.toURL());
Iterable<HTTPHeader> headers, String message, long timeout) throws IOException {
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public int tester(int trytry) {
return 5;
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
String message, long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
Headers headers, String message, long timeout) throws IOException {
Iterable<HTTPHeader> 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);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.PUT)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
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, new Headers.Builder().build(), message, timeout);
return issuePostRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload,
String resourceFormat, Headers headers, String message, long timeout) throws IOException {
String resourceFormat, Iterable<HTTPHeader> 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);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public boolean issueDeleteRequest(URI resourceUri, int timeout) throws IOException {
Request.Builder request = new Request.Builder().url(resourceUri.toURL()).delete();
return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout)
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.DELETE);
return executeFhirRequest(request, null, Collections.emptyList(), null, retryCount, timeout)
.isSuccessfulRequest();
}
public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat, int timeout) throws IOException {
Request.Builder request = new Request.Builder().url(resourceUri.toURL());
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout);
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName,
Resource resource, String resourceFormat, int timeout) 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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), 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, new Headers.Builder().build(), message, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request, String resourceFormat,
Headers headers, String message, int retryCount, long timeout) throws IOException {
return new FhirRequestBuilder(request, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat)
private static String getContentTypeWithDefaultCharset(String resourceFormat) {
return resourceFormat + ";charset=" + DEFAULT_CHARSET;
}
public <T extends Resource> Bundle executeBundleRequest(HTTPRequest request, String resourceFormat,
Iterable<HTTPHeader> headers, String message, int retryCount, long timeout) throws IOException {
return new FhirRequestBuilder(request, base).withLogger(logger).withResourceFormat(resourceFormat)
.withRetryCount(retryCount).withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS).executeAsBatch();
}
public <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, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat)
public <T extends Resource> ResourceRequest<T> executeFhirRequest(HTTPRequest request, String resourceFormat,
Iterable<HTTPHeader> headers, String message, int retryCount, long timeout) throws IOException {
return new FhirRequestBuilder(request, base).withLogger(logger).withResourceFormat(resourceFormat)
.withRetryCount(retryCount).withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS).execute();
}
}

View File

@ -6,7 +6,7 @@ import java.util.stream.Collectors;
import org.hl7.fhir.exceptions.FHIRException;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.utilities.http.HTTPHeader;
/**
* Generic Implementation of Client Headers.
@ -16,29 +16,29 @@ import okhttp3.internal.http2.Header;
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
private final ArrayList<HTTPHeader> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
public ClientHeaders(ArrayList<HTTPHeader> headers) {
this.headers = headers;
}
public ArrayList<Header> headers() {
public ArrayList<HTTPHeader> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @param header {@link HTTPHeader} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
public ClientHeaders addHeader(HTTPHeader header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", " + header.value + ">.");
throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", " + header.getValue() + ">.");
}
headers.add(header);
return this;
@ -47,10 +47,10 @@ public class ClientHeaders {
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @param headerList {@link List} of {@link HTTPHeader} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders addHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
@ -58,13 +58,13 @@ public class ClientHeaders {
/**
* Removes the passed in header from the list of stored headers.
*
* @param header {@link Header} to remove from the list.
* @param header {@link HTTPHeader} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the
* stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", " + header.value
throw new FHIRException("Attempting to remove header, <" + header.getName() + ", " + header.getValue()
+ ">, from GenericClientHeaders that is not currently stored.");
}
return this;
@ -73,17 +73,17 @@ public class ClientHeaders {
/**
* Removes the passed in headers from the list of stored headers.
*
* @param headerList {@link List} of {@link Header} to remove.
* @param headerList {@link List} of {@link HTTPHeader} to remove.
* @throws FHIRException if any of the headers passed in does not exist within
* the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders removeHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
* Clears all stored {@link HTTPHeader}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
@ -92,7 +92,7 @@ public class ClientHeaders {
@Override
public String toString() {
return this.headers.stream().map(header -> "\t" + header.name + ":" + header.value)
return this.headers.stream().map(header -> "\t" + header.getName() + ":" + header.getValue())
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -1,74 +0,0 @@
package org.hl7.fhir.r4.utils.client.network;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import org.hl7.fhir.utilities.ToolingClientLogger;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
public class FhirLoggingInterceptor implements Interceptor {
private ToolingClientLogger logger;
public FhirLoggingInterceptor(ToolingClientLogger logger) {
this.logger = logger;
}
public FhirLoggingInterceptor setLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@Override
public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException {
// Log Request
Request request = chain.request();
List<String> hdrs = new ArrayList<>();
for (String s : request.headers().toString().split("\\n")) {
hdrs.add(s.trim());
}
byte[] cnt = null;
if (request.body() != null) {
Buffer buf = new Buffer();
request.body().writeTo(buf);
cnt = buf.readByteArray();
}
if (logger != null) {
logger.logRequest(request.method(), request.url().toString(), hdrs, cnt);
}
// Log Response
Response response = null;
response = chain.proceed(chain.request());
MediaType contentType = null;
byte[] bodyBytes = null;
if (response.body() != null) {
contentType = response.body().contentType();
bodyBytes = response.body().bytes();
}
// Get Headers as List
List<String> headerList = new ArrayList<>();
Map<String, List<String>> headerMap = response.headers().toMultimap();
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
if (logger != null) {
logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, 0);
}
// Reading byte[] clears body. Need to recreate.
ResponseBody body = ResponseBody.create(bodyBytes, contentType);
return response.newBuilder().body(body).build();
}
}

View File

@ -1,16 +1,13 @@
package org.hl7.fhir.r4.utils.client.network;
import static org.hl7.fhir.r4.utils.OperationOutcomeUtilities.outcomeFromTextError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.formats.IParser;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.formats.XmlParser;
@ -22,31 +19,19 @@ import org.hl7.fhir.r4.utils.ResourceUtilities;
import org.hl7.fhir.r4.utils.client.EFhirClientException;
import org.hl7.fhir.r4.utils.client.ResourceFormat;
import org.hl7.fhir.utilities.MimeType;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.http.*;
import org.hl7.fhir.utilities.xhtml.XhtmlUtils;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
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 final HTTPRequest httpRequest;
private String resourceFormat = null;
private Headers headers = null;
private Iterable<HTTPHeader> headers = null;
private String message = null;
private int retryCount = 1;
/**
@ -60,63 +45,46 @@ public class FhirRequestBuilder {
private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
/**
* {@link FhirLoggingInterceptor} for log output.
* {@link ToolingClientLogger} for log output.
*/
private FhirLoggingInterceptor logger = null;
@Getter
@Setter
private ToolingClientLogger logger = null;
private String source;
public FhirRequestBuilder(Request.Builder httpRequest, String source) {
this.httpRequest = httpRequest;
public FhirRequestBuilder(HTTPRequest httpRequest, String source) {
this.source = source;
this.httpRequest = httpRequest;
}
/**
* Adds necessary default headers, formatting headers, and any passed in
* {@link Headers} to the passed in {@link okhttp3.Request.Builder}
* {@link HTTPHeader}s to the passed in {@link HTTPRequest}
*
* @param request {@link okhttp3.Request.Builder} to add headers to.
* @param request {@link HTTPRequest} to add headers to.
* @param format Expected {@link Resource} format.
* @param headers Any additional {@link Headers} to add to the request.
* @param headers Any additional {@link HTTPHeader}s to add to the request.
*/
protected static void formatHeaders(Request.Builder request, String format, Headers headers) {
addDefaultHeaders(request, headers);
if (format != null)
addResourceFormatHeaders(request, format);
if (headers != null)
addHeaders(request, headers);
protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable<HTTPHeader> headers) {
List<HTTPHeader> allHeaders = new ArrayList<>();
request.getHeaders().forEach(allHeaders::add);
if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add);
if (headers != null) headers.forEach(allHeaders::add);
return request.withHeaders(allHeaders);
}
/**
* Adds necessary headers for all REST requests.
* <li>User-Agent : hapi-fhir-tooling-client</li>
*
* @param request {@link Request.Builder} to add default headers to.
*/
protected static void addDefaultHeaders(Request.Builder request, Headers headers) {
if (headers == null || !headers.names().contains("User-Agent")) {
request.addHeader("User-Agent", "hapi-fhir-tooling-client");
protected static Iterable<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
List<HTTPHeader> headers = new ArrayList<>();
headers.add(new HTTPHeader("Accept", format));
if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
) {
headers.add(new HTTPHeader("Content-Type", format + ";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()));
return headers;
}
/**
@ -136,73 +104,24 @@ public class FhirRequestBuilder {
}
/**
* Extracts the 'location' header from the passes in {@link Headers}. If no
* Extracts the 'location' header from the passed {@link Iterable<HTTPHeader>}. 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
* @param headers {@link HTTPHeader} 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;
protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
if (locationHeader != null) {
return locationHeader;
}
return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
}
/**
* 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 (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
Authenticator proxyAuthenticator = getAuthenticator();
OkHttpClient.Builder builder = okHttpClient.newBuilder();
if (logger != null)
builder.addInterceptor(logger);
builder.addInterceptor(new RetryInterceptor(retryCount));
return builder.connectTimeout(timeout, timeoutUnit).addInterceptor(new RetryInterceptor(retryCount))
.connectTimeout(timeout, timeoutUnit).writeTimeout(timeout, timeoutUnit).readTimeout(timeout, timeoutUnit)
.proxyAuthenticator(proxyAuthenticator).build();
}
@Nonnull
private static Authenticator getAuthenticator() {
return (route, response) -> {
final String httpProxyUser = System.getProperty(HTTP_PROXY_USER);
final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS);
if (httpProxyUser != null && httpProxyPass != null) {
String credential = Credentials.basic(httpProxyUser, httpProxyPass);
return response.request().newBuilder().header(HEADER_PROXY_AUTH, credential).build();
}
return response.request().newBuilder().build();
};
protected ManagedFhirWebAccessor getManagedWebAccessor() {
return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
}
public FhirRequestBuilder withResourceFormat(String resourceFormat) {
@ -210,7 +129,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withHeaders(Headers headers) {
public FhirRequestBuilder withHeaders(Iterable<HTTPHeader> headers) {
this.headers = headers;
return this;
}
@ -225,7 +144,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withLogger(FhirLoggingInterceptor logger) {
public FhirRequestBuilder withLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@ -236,33 +155,31 @@ public class FhirRequestBuilder {
return this;
}
protected Request buildRequest() {
return httpRequest.build();
}
public <T extends Resource> ResourceRequest<T> execute() throws IOException {
formatHeaders(httpRequest, resourceFormat, headers);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
T resource = unmarshalReference(response, resourceFormat, null);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
return unmarshalFeed(response, resourceFormat);
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
return unmarshalFeed(response, resourceFormat);
}
/**
* Unmarshalls a resource from the response stream.
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(Response response, String format, String resourceType) {
int code = response.code();
protected <T extends Resource> T unmarshalReference(HTTPResult response, String format, String resourceType) {
int code = response.getCode();
boolean ok = code >= 200 && code < 300;
if (response.body() == null) {
if (response.getContent() == null) {
if (!ok) {
throw new EFhirClientException(response.message());
throw new EFhirClientException(response.getMessage());
} else {
return null;
}
@ -271,9 +188,9 @@ public class FhirRequestBuilder {
Resource resource = null;
try {
body = response.body().string();
String ct = response.header("Content-Type");
if (ct == null) {
body = response.getContentAsString();
String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
if (contentType == null) {
if (ok) {
resource = getParser(format).parse(body);
} else {
@ -282,10 +199,10 @@ public class FhirRequestBuilder {
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
}
} else {
if (ct.contains(";")) {
ct = ct.substring(0, ct.indexOf(";"));
if (contentType.contains(";")) {
contentType = contentType.substring(0, contentType.indexOf(";"));
}
switch (ct) {
switch (contentType) {
case "application/json":
case "application/fhir+json":
if (!format.contains("json")) {
@ -299,16 +216,16 @@ public class FhirRequestBuilder {
if (!format.contains("xml")) {
System.out.println("Got xml response expecting "+format+" from "+source+" with status "+code);
}
resource = getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(response.body().bytes());
resource = getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(response.getContent());
break;
case "text/plain":
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
break;
case "text/html" :
resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.body().string(), source));
resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source));
break;
default: // not sure what else to do?
System.out.println("Got content-type '"+ct+"' from "+source);
System.out.println("Got content-type '"+contentType+"' from "+source);
System.out.println(body);
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
}
@ -343,14 +260,14 @@ public class FhirRequestBuilder {
/**
* Unmarshalls Bundle from response stream.
*/
protected Bundle unmarshalFeed(Response response, String format) {
protected Bundle unmarshalFeed(HTTPResult response, String format) {
return unmarshalReference(response, format, "Bundle");
}
/**
* Returns the appropriate parser based on the format type passed in. Defaults
* to XML parser if a blank format is provided...because reasons.
* <p>
* <p/>
* Currently supports only "json" and "xml" formats.
*
* @param format One of "json" or "xml".

View File

@ -1,67 +0,0 @@
package org.hl7.fhir.r4.utils.client.network;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* 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 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,224 @@
package org.hl7.fhir.r4.utils.client;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.utils.client.network.Client;
import org.hl7.fhir.r4.utils.client.network.ResourceRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FhirToolingClientTest {
String TX_ADDR = "http://tx.fhir.org";
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases");
private Client mockClient;
private FHIRToolingClient toolingClient;
@Captor
private ArgumentCaptor<Iterable<HTTPHeader>> headersArgumentCaptor;
@BeforeEach
void setUp() throws IOException, URISyntaxException {
MockitoAnnotations.openMocks(this);
mockClient = Mockito.mock(Client.class);
ResourceRequest<Resource> resourceResourceRequest = new ResourceRequest<>(generateBundle(), 200, "");
// GET
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location"));
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
// PUT
Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
// POST
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
Mockito
.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location"));
// BUNDLE REQ
Mockito
.when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
.thenReturn(generateBundle());
toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases");
toolingClient.setClient(mockClient);
}
private List<HTTPHeader> getHeaders() {
return new ArrayList<>(Arrays.asList(h1, h2, h3));
}
private List<HTTPHeader> getHeadersWithAgent() {
return new ArrayList<>(Arrays.asList(h1, h2, h3, agentHeader));
}
private Bundle generateBundle() {
Patient patient = generatePatient();
Observation observation = generateObservation();
// The observation refers to the patient using the ID, which is already
// set to a temporary UUID
observation.setSubject(new Reference(patient.getIdElement().getValue()));
// Create a bundle that will be used as a transaction
Bundle bundle = new Bundle();
// Add the patient as an entry.
bundle.addEntry().setFullUrl(patient.getIdElement().getValue()).setResource(patient).getRequest().setUrl("Patient")
.setIfNoneExist("identifier=http://acme.org/mrns|12345").setMethod(Bundle.HTTPVerb.POST);
return bundle;
}
private Patient generatePatient() {
// Create a patient object
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://acme.org/mrns").setValue("12345");
patient.addName().setFamily("Jameson").addGiven("J").addGiven("Jonah");
patient.setGender(Enumerations.AdministrativeGender.MALE);
// Give the patient a temporary UUID so that other resources in
// the transaction can refer to it
patient.setId(IdType.newRandomUuid());
return patient;
}
private Observation generateObservation() {
// Create an observation object
Observation observation = new Observation();
observation.getCode().addCoding().setSystem("http://loinc.org").setCode("789-8")
.setDisplay("Erythrocytes [#/volume] in Blood by Automated count");
observation.setValue(new Quantity().setValue(4.12).setUnit("10 trillion/L").setSystem("http://unitsofmeasure.org")
.setCode("10*12/L"));
return observation;
}
private void checkHeaders(Iterable<HTTPHeader> argumentCaptorValue) {
List<HTTPHeader> capturedHeaders = new ArrayList<>();
argumentCaptorValue.forEach(capturedHeaders::add);
getHeadersWithAgent().forEach(header -> {
assertTrue(capturedHeaders.contains(header));
});
}
@Test
void getTerminologyCapabilities() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatement() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatement();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatementQuick() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatementQuick();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void read() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.read(Patient.class, "id");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void vread() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.vread(Patient.class, "id", "version");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCanonical() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCanonical(Patient.class, "canonicalURL");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void update() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.update(generatePatient());
Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void validate() throws IOException {
toolingClient.setClientHeaders(getHeaders());
toolingClient.validate(Patient.class, generatePatient(), "id");
Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
}

View File

@ -0,0 +1,104 @@
package org.hl7.fhir.r4.utils.client.network;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
public class ClientHeadersTest {
ClientHeaders clientHeaders;
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
@BeforeEach
void setUp() {
clientHeaders = new ClientHeaders();
}
@Test
@DisplayName("Happy path add headers individually.")
void addHeader() {
clientHeaders.addHeader(h1);
Assertions.assertEquals(1, clientHeaders.headers().size());
clientHeaders.addHeader(h2);
Assertions.assertEquals(2, clientHeaders.headers().size());
}
@Test
@DisplayName("Test duplicate header added individually throws FHIRException.")
void addHeaderDuplicateAdd() {
clientHeaders.addHeader(h1);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeader(h1));
}
@Test
@DisplayName("Happy path add headers as list.")
void addHeaders() {
List<HTTPHeader> headersList = Arrays.asList(h1, h2, h3);
clientHeaders.addHeaders(headersList);
Assertions.assertEquals(3, clientHeaders.headers().size());
Assertions.assertEquals(headersList, clientHeaders.headers());
}
@Test
@DisplayName("Happy path add headers as list.")
void addHeadersDuplicateAdd() {
List<HTTPHeader> headersList = Arrays.asList(h1, h2, h1);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList));
}
@Test
@DisplayName("Happy path remove existing header.")
void removeHeader() {
clientHeaders.addHeader(h1);
clientHeaders.addHeader(h2);
clientHeaders.addHeader(h3);
clientHeaders.removeHeader(h2);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.removeHeader(new HTTPHeader("header3", "value3"));
Assertions.assertEquals(1, clientHeaders.headers().size());
}
@Test
@DisplayName("Remove header not contained in list.")
void removeHeaderUnknown() {
clientHeaders.addHeader(h1);
clientHeaders.addHeader(h2);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeader(h3));
}
@Test
@DisplayName("Happy path remove list of existing headers.")
void removeHeaders() {
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h2, h3);
List<HTTPHeader> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
clientHeaders.removeHeaders(headersToRemoveList);
Assertions.assertEquals(1, clientHeaders.headers().size());
}
@Test
@DisplayName("Remove list containing unknown header.")
void removeHeadersUnknown() {
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h3);
List<HTTPHeader> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList));
}
@Test
void clearHeaders() {
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.clearHeaders();
Assertions.assertEquals(0, clientHeaders.headers().size());
}
}

View File

@ -0,0 +1,140 @@
package org.hl7.fhir.r4.utils.client.network;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.RecordedRequest;
import org.hl7.fhir.r4.context.HTMLClientLogger;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.model.*;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class ClientTest {
private static final long TIMEOUT = 5000;
private MockWebServer server;
private HttpUrl serverUrl;
private Client client;
private final Address address = new Address().setCity("Toronto").setState("Ontario").setCountry("Canada");
private final HumanName humanName = new HumanName().addGiven("Mark").setFamily("Iantorno");
private final 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, 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, 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, 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, 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, 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.");
}
@Test
@DisplayName("Testing the logger works.")
void test_logger() throws IOException, URISyntaxException, InterruptedException {
byte[] payload = ByteUtils.resourceToByteArray(patient, true, false, false);
server.enqueue(new MockResponse().setResponseCode(200).setBody(new String(payload)));
HTMLClientLogger mockLogger = Mockito.mock(HTMLClientLogger.class);
client.setLogger(mockLogger);
client.issuePostRequest(new URI(serverUrl.toString()), payload, "xml", null, TIMEOUT);
server.takeRequest();
Mockito.verify(mockLogger, Mockito.times(1)).logRequest(Mockito.anyString(), Mockito.anyString(), Mockito.anyList(),
Mockito.any());
Mockito.verify(mockLogger, Mockito.times(1)).logResponse(Mockito.anyString(), Mockito.anyList(), Mockito.any(), Mockito.anyLong());
}
}

View File

@ -0,0 +1,128 @@
package org.hl7.fhir.r4.utils.client.network;
import okhttp3.Request;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPHeaderUtil;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class FhirRequestBuilderTest {
@Test
@DisplayName("Test resource format headers are added correctly.")
void addResourceFormatHeadersGET() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET);
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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.assertNull(headersMap.get("Content-Type"), "Content-Type header null.");
}
@Test
@DisplayName("Test resource format headers are added correctly (POST).")
void addResourceFormatHeadersPOST() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST);
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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 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";
Iterable<HTTPHeader> headers = List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader));
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";
Iterable<HTTPHeader> headers = List.of(new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader));
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";
Iterable<HTTPHeader> headers = List.of(
new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader),
new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)
);
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@Test
@DisplayName("Test that getLocationHeader returns null when no location available.")
void getLocationHeaderWhenNoLocationSet() {
Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList()));
}
}

View File

@ -34,6 +34,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -1,22 +1,14 @@
package org.hl7.fhir.r4b.terminologies;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -156,9 +148,9 @@ public class TerminologyCacheManager {
String url = "https://tx.fhir.org/post/tx-cache/" + ghOrg + "/" + ghRepo + "/" + ghBranch + ".zip";
System.out.println("Sending tx-cache to " + url + " (" + Utilities.describeSize(bs.toByteArray().length) + ")");
HTTPResult res = ManagedWebAccess.builder()
HTTPResult res = ManagedWebAccess.accessor()
.withBasicAuth(token.substring(0, token.indexOf(':')), token.substring(token.indexOf(':') + 1))
.withAccept("application/zip").put(url, bs.toByteArray(), null);
.put(url, bs.toByteArray(), null, "application/zip");
if (res.getCode() >= 300) {
System.out.println("sending cache failed: " + res.getCode());
} else {

View File

@ -1,7 +1,8 @@
package org.hl7.fhir.r4b.utils.client;
import okhttp3.Headers;
import okhttp3.internal.http2.Header;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.exceptions.FHIRException;
/*
@ -37,14 +38,13 @@ import org.hl7.fhir.r4b.model.*;
import org.hl7.fhir.r4b.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4b.utils.client.network.ByteUtils;
import org.hl7.fhir.r4b.utils.client.network.Client;
import org.hl7.fhir.r4b.utils.client.network.ClientHeaders;
import org.hl7.fhir.r4b.utils.client.network.ResourceRequest;
import org.hl7.fhir.utilities.FHIRBaseToolingClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.http.HTTPHeader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
@ -90,12 +90,21 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
private ResourceFormat preferredResourceFormat;
private int maxResultSetSize = -1;// _count
private CapabilityStatement capabilities;
@Getter
@Setter
private Client client = new Client();
private ArrayList<Header> headers = new ArrayList<>();
private String username;
private String password;
private List<HTTPHeader> headers = new ArrayList<>();
@Setter
@Getter
private String userAgent;
@Setter
private String acceptLanguage;
@Setter
private String contentLanguage;
// Pass endpoint for client - URI
public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
preferredResourceFormat = ResourceFormat.RESOURCE_XML;
@ -111,14 +120,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
checkCapabilities();
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
private void checkCapabilities() {
try {
capabilities = getCapabilitiesStatementQuick();
@ -146,7 +147,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
TerminologyCapabilities capabilities = null;
try {
capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
getPreferredResourceFormat(), generateHeaders(), "TerminologyCapabilities", timeoutNormal).getReference();
getPreferredResourceFormat(), generateHeaders(false), "TerminologyCapabilities", timeoutNormal).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's terminology capabilities", e);
}
@ -157,7 +158,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
CapabilityStatement conformance = null;
try {
conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
getPreferredResourceFormat(), generateHeaders(), "CapabilitiesStatement", timeoutNormal).getReference();
getPreferredResourceFormat(), generateHeaders(false), "CapabilitiesStatement", timeoutNormal).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's conformance statement", e);
}
@ -169,7 +170,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
return capabilities;
try {
capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
getPreferredResourceFormat(), generateHeaders(), "CapabilitiesStatement-Quick", timeoutNormal)
getPreferredResourceFormat(), generateHeaders(false), "CapabilitiesStatement-Quick", timeoutNormal)
.getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's capability statement: " + e.getMessage(), e);
@ -181,7 +182,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
ResourceRequest<T> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
getPreferredResourceFormat(), generateHeaders(), "Read " + resourceClass.getName() + "/" + id,
getPreferredResourceFormat(), generateHeaders(false), "Read " + resourceClass.getName() + "/" + id,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
@ -198,7 +199,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
try {
result = client.issueGetResourceRequest(
resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
getPreferredResourceFormat(), generateHeaders(),
getPreferredResourceFormat(), generateHeaders(false),
"VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
@ -215,7 +216,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
try {
result = client.issueGetResourceRequest(
resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
getPreferredResourceFormat(), generateHeaders(), "Read " + resourceClass.getName() + "?url=" + canonicalURL,
getPreferredResourceFormat(), generateHeaders(false), "Read " + resourceClass.getName() + "?url=" + canonicalURL,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
@ -238,7 +239,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
result = client.issuePutRequest(
resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
getPreferredResourceFormat(), generateHeaders(), "Update " + resource.fhirType() + "/" + resource.getId(),
getPreferredResourceFormat(), generateHeaders(true), "Update " + resource.fhirType() + "/" + resource.getId(),
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
@ -267,7 +268,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
try {
result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
getPreferredResourceFormat(), generateHeaders(), "Update " + resource.fhirType() + "/" + id,
getPreferredResourceFormat(), generateHeaders(true), "Update " + resource.fhirType() + "/" + id,
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
@ -305,10 +306,10 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
if (complex) {
byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true);
result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(),
result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(true),
"POST " + resourceClass.getName() + "/$" + name, timeoutLong);
} else {
result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(),
result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(false),
"GET " + resourceClass.getName() + "/$" + name, timeoutLong);
}
if (result.isUnsuccessfulRequest()) {
@ -334,7 +335,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
try {
transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(),
ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat()), false),
getPreferredResourceFormat(), generateHeaders(), "transaction",
getPreferredResourceFormat(), generateHeaders(true), "transaction",
timeoutOperation + (timeoutEntry * batch.getEntry().size()));
} catch (Exception e) {
handleException("An error occurred trying to process this transaction request", e);
@ -348,7 +349,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
try {
result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false),
getPreferredResourceFormat(), generateHeaders(),
getPreferredResourceFormat(), generateHeaders(true),
"POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
@ -361,10 +362,14 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
}
/**
* Helper method to prevent nesting of previously thrown EFhirClientExceptions
* Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of
* EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the
* cause.
*
* @param e
* @throws EFhirClientException
* @param message The EFhirClientException message.
* @param e The exception
* @throws EFhirClientException EFhirClientException representing the exception.
*/
protected void handleException(String message, Exception e) throws EFhirClientException {
if (e instanceof EFhirClientException) {
@ -378,8 +383,8 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
* Helper method to determine whether desired resource representation is Json or
* XML.
*
* @param format
* @return
* @param format the format
* @return true if the format is JSON, false otherwise
*/
protected boolean isJson(String format) {
boolean isJson = false;
@ -406,7 +411,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
try {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), getPreferredResourceFormat(),
generateHeaders(), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand);
generateHeaders(true), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
@ -421,7 +426,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
org.hl7.fhir.r4b.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
getPreferredResourceFormat(), generateHeaders(), "CodeSystem/$lookup", timeoutNormal);
getPreferredResourceFormat(), generateHeaders(false), "CodeSystem/$lookup", timeoutNormal);
} catch (IOException e) {
e.printStackTrace();
}
@ -443,7 +448,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), getPreferredResourceFormat(),
generateHeaders(), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand);
generateHeaders(true), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
@ -466,7 +471,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
result = client.issuePostRequest(
resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
getPreferredResourceFormat(), generateHeaders(), "Closure?name=" + name, timeoutNormal);
getPreferredResourceFormat(), generateHeaders(true), "Closure?name=" + name, timeoutNormal);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
@ -486,7 +491,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
result = client.issuePostRequest(
resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
getPreferredResourceFormat(), generateHeaders(), "UpdateClosure?name=" + name, timeoutOperation);
getPreferredResourceFormat(), generateHeaders(true), "UpdateClosure?name=" + name, timeoutOperation);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
@ -497,22 +502,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
return result == null ? null : (ConceptMap) result.getPayload();
}
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 long getTimeout() {
return client.getTimeout();
}
@ -537,42 +526,27 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{
client.setRetryCount(retryCount);
}
public void setClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
public void setClientHeaders(Iterable<HTTPHeader> headers) {
this.headers = new ArrayList<>();
headers.forEach(this.headers::add);
}
private Headers generateHeaders() {
Headers.Builder builder = new Headers.Builder();
// Add basic auth header if it exists
if (basicAuthHeaderExists()) {
builder.add(getAuthorizationHeader().toString());
}
private Iterable<HTTPHeader> generateHeaders(boolean hasBody) {
// Add any other headers
if (this.headers != null) {
this.headers.forEach(header -> builder.add(header.toString()));
}
List<HTTPHeader> headers = new ArrayList<>(this.headers);
if (!Utilities.noString(userAgent)) {
builder.add("User-Agent: " + userAgent);
headers.add(new HTTPHeader("User-Agent",userAgent));
}
return builder.build();
}
public boolean basicAuthHeaderExists() {
return (username != null) && (password != null);
}
if (!Utilities.noString(acceptLanguage)) {
headers.add(new HTTPHeader("Accept-Language", acceptLanguage));
}
public Header getAuthorizationHeader() {
String usernamePassword = username + ":" + password;
String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
return new Header("Authorization", "Basic " + base64usernamePassword);
}
if (hasBody && !Utilities.noString(contentLanguage)) {
headers.add(new HTTPHeader("Content-Language",contentLanguage));
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
return headers;
}
public String getServerVersion() {

View File

@ -1,16 +1,17 @@
package org.hl7.fhir.r4b.utils.client.network;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.r4b.model.Bundle;
import org.hl7.fhir.r4b.model.Resource;
import org.hl7.fhir.r4b.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -18,144 +19,132 @@ public class Client {
public static final String DEFAULT_CHARSET = "UTF-8";
private static final long DEFAULT_TIMEOUT = 5000;
@Getter @Setter
private ToolingClientLogger logger;
private FhirLoggingInterceptor fhirLoggingInterceptor;
@Setter
@Getter
private int retryCount;
@Setter
@Getter
private long timeout = DEFAULT_TIMEOUT;
private byte[] payload;
@Setter @Getter
private String base;
public String getBase() {
return base;
}
public void setBase(String base) {
this.base = base;
}
public ToolingClientLogger getLogger() {
return logger;
}
public void setLogger(ToolingClientLogger logger) {
this.logger = logger;
this.fhirLoggingInterceptor = new FhirLoggingInterceptor(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 {
this.payload = null;
Request.Builder request = new Request.Builder().method("OPTIONS", null).url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(optionsUri.toURL())
.withMethod(HTTPRequest.HttpMethod.OPTIONS);
return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat,
Headers headers, String message, long timeout) throws IOException {
this.payload = null;
Request.Builder request = new Request.Builder().url(resourceUri.toURL());
Iterable<HTTPHeader> headers, String message, long timeout) throws IOException {
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public int tester(int trytry) {
return 5;
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
String message, long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
Headers headers, String message, long timeout) throws IOException {
Iterable<HTTPHeader> headers, String message, long timeout) throws IOException {
if (payload == null)
throw new EFhirClientException("PUT requests require a non-null payload");
this.payload = payload;
RequestBody body = RequestBody.create(payload);
Request.Builder request = new Request.Builder().url(resourceUri.toURL()).put(body);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.PUT)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
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, new Headers.Builder().build(), message, timeout);
return issuePostRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload,
String resourceFormat, Headers headers, String message, long timeout) throws IOException {
String resourceFormat, Iterable<HTTPHeader> headers, String message, long timeout) throws IOException {
if (payload == null)
throw new EFhirClientException("POST requests require a non-null payload");
this.payload = payload;
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
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, new Headers.Builder().build(), null, retryCount, timeout)
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.DELETE);
return executeFhirRequest(request, null, Collections.emptyList(), 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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), 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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, Headers headers,
public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, Iterable<HTTPHeader> headers,
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);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeBundleRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request, String resourceFormat,
Headers headers, String message, int retryCount, long timeout) throws IOException {
return new FhirRequestBuilder(request, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat)
private static String getContentTypeWithDefaultCharset(String resourceFormat) {
return resourceFormat + ";charset=" + DEFAULT_CHARSET;
}
public <T extends Resource> Bundle executeBundleRequest(HTTPRequest request, String resourceFormat,
Iterable<HTTPHeader> headers, String message, int retryCount, long timeout) throws IOException {
return new FhirRequestBuilder(request, base).withLogger(logger).withResourceFormat(resourceFormat)
.withRetryCount(retryCount).withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS).executeAsBatch();
}
public <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, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat)
public <T extends Resource> ResourceRequest<T> executeFhirRequest(HTTPRequest request, String resourceFormat,
Iterable<HTTPHeader> headers, String message, int retryCount, long timeout) throws IOException {
return new FhirRequestBuilder(request, base).withLogger(logger).withResourceFormat(resourceFormat)
.withRetryCount(retryCount).withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS).execute();
}
}

View File

@ -1,7 +1,8 @@
package org.hl7.fhir.r4b.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.http.HTTPHeader;
import java.util.ArrayList;
import java.util.List;
@ -9,35 +10,35 @@ import java.util.stream.Collectors;
/**
* Generic Implementation of Client Headers.
*
* <p/>
* Stores a list of headers for HTTP calls to the TX server. Users can implement
* their own instance if they desire specific, custom behavior.
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
private final ArrayList<HTTPHeader> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
public ClientHeaders(ArrayList<HTTPHeader> headers) {
this.headers = headers;
}
public ArrayList<Header> headers() {
public ArrayList<HTTPHeader> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @param header {@link HTTPHeader} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
public ClientHeaders addHeader(HTTPHeader header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", " + header.value + ">.");
throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", " + header.getValue() + ">.");
}
headers.add(header);
return this;
@ -46,10 +47,10 @@ public class ClientHeaders {
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @param headerList {@link List} of {@link HTTPHeader} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders addHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
@ -57,13 +58,13 @@ public class ClientHeaders {
/**
* Removes the passed in header from the list of stored headers.
*
* @param header {@link Header} to remove from the list.
* @param header {@link HTTPHeader} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the
* stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", " + header.value
throw new FHIRException("Attempting to remove header, <" + header.getName() + ", " + header.getValue()
+ ">, from GenericClientHeaders that is not currently stored.");
}
return this;
@ -72,17 +73,17 @@ public class ClientHeaders {
/**
* Removes the passed in headers from the list of stored headers.
*
* @param headerList {@link List} of {@link Header} to remove.
* @param headerList {@link List} of {@link HTTPHeader} to remove.
* @throws FHIRException if any of the headers passed in does not exist within
* the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders removeHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
* Clears all stored {@link HTTPHeader}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
@ -91,7 +92,7 @@ public class ClientHeaders {
@Override
public String toString() {
return this.headers.stream().map(header -> "\t" + header.name + ":" + header.value)
return this.headers.stream().map(header -> "\t" + header.getName() + ":" + header.getValue())
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -1,69 +0,0 @@
package org.hl7.fhir.r4b.utils.client.network;
import okhttp3.*;
import okio.Buffer;
import org.hl7.fhir.utilities.ToolingClientLogger;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class FhirLoggingInterceptor implements Interceptor {
private ToolingClientLogger logger;
public FhirLoggingInterceptor(ToolingClientLogger logger) {
this.logger = logger;
}
public FhirLoggingInterceptor setLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@Override
public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException {
// Log Request
Request request = chain.request();
List<String> hdrs = new ArrayList<>();
for (String s : request.headers().toString().split("\\n")) {
hdrs.add(s.trim());
}
byte[] cnt = null;
if (request.body() != null) {
Buffer buf = new Buffer();
request.body().writeTo(buf);
cnt = buf.readByteArray();
}
if (logger != null) {
logger.logRequest(request.method(), request.url().toString(), hdrs, cnt);
}
// Log Response
Response response = null;
response = chain.proceed(chain.request());
MediaType contentType = null;
byte[] bodyBytes = null;
if (response.body() != null) {
contentType = response.body().contentType();
bodyBytes = response.body().bytes();
}
// Get Headers as List
List<String> headerList = new ArrayList<>();
Map<String, List<String>> headerMap = response.headers().toMultimap();
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
if (logger != null) {
logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, 0);
}
// Reading byte[] clears body. Need to recreate.
ResponseBody body = ResponseBody.create(bodyBytes, contentType);
return response.newBuilder().body(body).build();
}
}

View File

@ -1,11 +1,10 @@
package org.hl7.fhir.r4b.utils.client.network;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4b.formats.IParser;
@ -18,15 +17,10 @@ import org.hl7.fhir.r4b.utils.OperationOutcomeUtilities;
import org.hl7.fhir.r4b.utils.ResourceUtilities;
import org.hl7.fhir.r4b.utils.client.EFhirClientException;
import org.hl7.fhir.r4b.utils.client.ResourceFormat;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.http.*;
import org.hl7.fhir.utilities.xhtml.XhtmlUtils;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class FhirRequestBuilder {
protected static final String HTTP_PROXY_USER = "http.proxyUser";
@ -35,13 +29,10 @@ public class FhirRequestBuilder {
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 final HTTPRequest httpRequest;
private String resourceFormat = null;
private Headers headers = null;
private Iterable<HTTPHeader> headers = null;
private String message = null;
private int retryCount = 1;
/**
@ -55,63 +46,43 @@ public class FhirRequestBuilder {
private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
/**
* {@link FhirLoggingInterceptor} for log output.
* {@link ToolingClientLogger} for log output.
*/
private FhirLoggingInterceptor logger = null;
private ToolingClientLogger logger = null;
private String source;
public FhirRequestBuilder(Request.Builder httpRequest, String source) {
this.httpRequest = httpRequest;
public FhirRequestBuilder(HTTPRequest httpRequest, String source) {
this.source = source;
this.httpRequest = httpRequest;
}
/**
* Adds necessary default headers, formatting headers, and any passed in
* {@link Headers} to the passed in {@link okhttp3.Request.Builder}
* {@link HTTPHeader} 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.
* @param headers Any additional {@link HTTPHeader} to add to the request.
*/
protected static void formatHeaders(Request.Builder request, String format, Headers headers) {
addDefaultHeaders(request, headers);
if (format != null)
addResourceFormatHeaders(request, format);
if (headers != null)
addHeaders(request, headers);
protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable<HTTPHeader> headers) {
List<HTTPHeader> allHeaders = new ArrayList<>();
request.getHeaders().forEach(allHeaders::add);
if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add);
if (headers != null) headers.forEach(allHeaders::add);
return request.withHeaders(allHeaders);
}
/**
* Adds necessary headers for all REST requests.
* <li>User-Agent : hapi-fhir-tooling-client</li>
*
* @param request {@link Request.Builder} to add default headers to.
*/
protected static void addDefaultHeaders(Request.Builder request, Headers headers) {
if (headers == null || !headers.names().contains("User-Agent")) {
request.addHeader("User-Agent", "hapi-fhir-tooling-client");
protected static Iterable<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
List<HTTPHeader> headers = new ArrayList<>();
headers.add(new HTTPHeader("Accept", format));
if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
) {
headers.add( new HTTPHeader("Content-Type", format + ";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()));
return headers;
}
/**
@ -131,68 +102,24 @@ public class FhirRequestBuilder {
}
/**
* Extracts the 'location' header from the passes in {@link Headers}. If no
* Extracts the 'location' header from the passed in {@link HTTPHeader}s. 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
* @param headers {@link HTTPHeader}s 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;
protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
if (locationHeader != null) {
return locationHeader;
}
return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
}
/**
* 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 = getAuthenticator();
OkHttpClient.Builder builder = okHttpClient.newBuilder();
if (logger != null)
builder.addInterceptor(logger);
builder.addInterceptor(new RetryInterceptor(retryCount));
return builder.connectTimeout(timeout, timeoutUnit).writeTimeout(timeout, timeoutUnit)
.readTimeout(timeout, timeoutUnit).proxyAuthenticator(proxyAuthenticator).build();
}
@Nonnull
private static Authenticator getAuthenticator() {
return (route, response) -> {
final String httpProxyUser = System.getProperty(HTTP_PROXY_USER);
final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS);
if (httpProxyUser != null && httpProxyPass != null) {
String credential = Credentials.basic(httpProxyUser, httpProxyPass);
return response.request().newBuilder().header(HEADER_PROXY_AUTH, credential).build();
}
return response.request().newBuilder().build();
};
protected ManagedFhirWebAccessor getManagedWebAccessor() {
return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
}
public FhirRequestBuilder withResourceFormat(String resourceFormat) {
@ -200,7 +127,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withHeaders(Headers headers) {
public FhirRequestBuilder withHeaders(Iterable<HTTPHeader> headers) {
this.headers = headers;
return this;
}
@ -215,7 +142,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withLogger(FhirLoggingInterceptor logger) {
public FhirRequestBuilder withLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@ -226,33 +153,28 @@ public class FhirRequestBuilder {
return this;
}
protected Request buildRequest() {
return httpRequest.build();
}
public <T extends Resource> ResourceRequest<T> execute() throws IOException {
formatHeaders(httpRequest, resourceFormat, headers);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
T resource = unmarshalReference(response, resourceFormat, null);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
return unmarshalFeed(response, resourceFormat);
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);return unmarshalFeed(response, resourceFormat);
}
/**
* Unmarshalls a resource from the response stream.
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(Response response, String format, String resourceType) {
int code = response.code();
protected <T extends Resource> T unmarshalReference(HTTPResult response, String format, String resourceType) {
int code = response.getCode();
boolean ok = code >= 200 && code < 300;
if (response.body() == null) {
if (response.getContent() == null) {
if (!ok) {
throw new EFhirClientException(response.message());
throw new EFhirClientException(response.getMessage());
} else {
return null;
}
@ -261,9 +183,9 @@ public class FhirRequestBuilder {
Resource resource = null;
try {
body = response.body().string();
String ct = response.header("Content-Type");
if (ct == null) {
body = response.getContentAsString();
String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
if (contentType == null) {
if (ok) {
resource = getParser(format).parse(body);
} else {
@ -272,10 +194,10 @@ public class FhirRequestBuilder {
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
}
} else {
if (ct.contains(";")) {
ct = ct.substring(0, ct.indexOf(";"));
if (contentType.contains(";")) {
contentType = contentType.substring(0, contentType.indexOf(";"));
}
switch (ct) {
switch (contentType) {
case "application/json":
case "application/fhir+json":
if (!format.contains("json")) {
@ -295,10 +217,10 @@ public class FhirRequestBuilder {
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
break;
case "text/html" :
resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.body().string(), source));
resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source));
break;
default: // not sure what else to do?
System.out.println("Got content-type '"+ct+"' from "+source);
System.out.println("Got content-type '"+contentType+"' from "+source);
System.out.println(body);
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
}
@ -333,7 +255,7 @@ public class FhirRequestBuilder {
/**
* Unmarshalls Bundle from response stream.
*/
protected Bundle unmarshalFeed(Response response, String format) {
protected Bundle unmarshalFeed(HTTPResult response, String format) {
return unmarshalReference(response, format, "Bundle");
}

View File

@ -1,67 +0,0 @@
package org.hl7.fhir.r4b.utils.client.network;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
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 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

@ -1,79 +1,89 @@
package org.hl7.fhir.r4b.utils.client;
import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.r4b.model.*;
import org.hl7.fhir.r4b.utils.client.network.Client;
import org.hl7.fhir.r4b.utils.client.network.ResourceRequest;
import org.junit.jupiter.api.Assertions;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FHIRToolingClientTest {
String TX_ADDR = "http://tx.fhir.org";
Header h1 = new Header("header1", "value1");
Header h2 = new Header("header2", "value2");
Header h3 = new Header("header3", "value3");
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases");
private Client mockClient;
private FHIRToolingClient toolingClient;
@Captor
private ArgumentCaptor<Iterable<HTTPHeader>> headersArgumentCaptor;
@BeforeEach
void setUp() throws IOException, URISyntaxException {
MockitoAnnotations.openMocks(this);
mockClient = Mockito.mock(Client.class);
ResourceRequest<Resource> resourceResourceRequest = new ResourceRequest<>(generateBundle(), 200, "");
// GET
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location"));
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
Mockito
.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
// PUT
Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
// POST
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest);
Mockito
.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.contains("validate"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location"));
// BUNDLE REQ
Mockito
.when(mockClient.executeBundleRequest(Mockito.any(Request.Builder.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
.when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
.thenReturn(generateBundle());
toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases");
toolingClient.setClient(mockClient);
}
private ArrayList<Header> getHeaders() {
private List<HTTPHeader> getHeaders() {
return new ArrayList<>(Arrays.asList(h1, h2, h3));
}
private List<HTTPHeader> getHeadersWithAgent() {
return new ArrayList<>(Arrays.asList(h1, h2, h3, agentHeader));
}
private Bundle generateBundle() {
Patient patient = generatePatient();
Observation observation = generateObservation();
@ -115,108 +125,102 @@ class FHIRToolingClientTest {
return observation;
}
private void checkHeaders(Headers argumentCaptorValue) {
getHeaders().forEach(header -> {
// System.out.println("Checking header <" + header.component1().utf8() + ", " + header.component2().utf8() + ">");
Assertions.assertEquals(argumentCaptorValue.get(header.component1().utf8()), header.component2().utf8());
private void checkHeaders(Iterable<HTTPHeader> argumentCaptorValue) {
List<HTTPHeader> capturedHeaders = new ArrayList<>();
argumentCaptorValue.forEach(capturedHeaders::add);
getHeadersWithAgent().forEach(header -> {
assertTrue(capturedHeaders.contains(header));
});
}
@Test
void getTerminologyCapabilities() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatement() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatement();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatementQuick() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatementQuick();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void read() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.read(Patient.class, "id");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void vread() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.vread(Patient.class, "id", "version");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCanonical() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCanonical(Patient.class, "canonicalURL");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void update() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.update(generatePatient());
Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void validate() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.validate(Patient.class, generatePatient(), "id");
Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
}

View File

@ -1,7 +1,7 @@
package org.hl7.fhir.r4b.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@ -14,9 +14,9 @@ class ClientHeadersTest {
ClientHeaders clientHeaders;
Header h1 = new Header("header1", "value1");
Header h2 = new Header("header2", "value2");
Header h3 = new Header("header3", "value3");
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
@BeforeEach
void setUp() {
@ -42,7 +42,7 @@ class ClientHeadersTest {
@Test
@DisplayName("Happy path add headers as list.")
void addHeaders() {
List<Header> headersList = Arrays.asList(h1, h2, h3);
List<HTTPHeader> headersList = Arrays.asList(h1, h2, h3);
clientHeaders.addHeaders(headersList);
Assertions.assertEquals(3, clientHeaders.headers().size());
Assertions.assertEquals(headersList, clientHeaders.headers());
@ -51,7 +51,7 @@ class ClientHeadersTest {
@Test
@DisplayName("Happy path add headers as list.")
void addHeadersDuplicateAdd() {
List<Header> headersList = Arrays.asList(h1, h2, h1);
List<HTTPHeader> headersList = Arrays.asList(h1, h2, h1);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList));
}
@ -63,7 +63,7 @@ class ClientHeadersTest {
clientHeaders.addHeader(h3);
clientHeaders.removeHeader(h2);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.removeHeader(new Header("header3", "value3"));
clientHeaders.removeHeader(new HTTPHeader("header3", "value3"));
Assertions.assertEquals(1, clientHeaders.headers().size());
}
@ -78,8 +78,8 @@ class ClientHeadersTest {
@Test
@DisplayName("Happy path remove list of existing headers.")
void removeHeaders() {
List<Header> headersToAddList = Arrays.asList(h1, h2, h3);
List<Header> headersToRemoveList = Arrays.asList(h2, h3);
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h2, h3);
List<HTTPHeader> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
clientHeaders.removeHeaders(headersToRemoveList);
Assertions.assertEquals(1, clientHeaders.headers().size());
@ -88,15 +88,15 @@ class ClientHeadersTest {
@Test
@DisplayName("Remove list containing unknown header.")
void removeHeadersUnknown() {
List<Header> headersToAddList = Arrays.asList(h1, h3);
List<Header> headersToRemoveList = Arrays.asList(h2, h3);
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h3);
List<HTTPHeader> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList));
}
@Test
void clearHeaders() {
List<Header> headersToAddList = Arrays.asList(h1, h3);
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.clearHeaders();

View File

@ -1,71 +1,51 @@
package org.hl7.fhir.r4b.utils.client.network;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.hl7.fhir.r4b.model.OperationOutcome;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPHeaderUtil;
import org.hl7.fhir.utilities.http.HTTPRequest;
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;
import java.util.*;
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, null);
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\".");
}
@Test
@DisplayName("Test resource format headers are added correctly.")
void addResourceFormatHeaders() {
void addResourceFormatHeadersGET() {
String testFormat = "yaml";
Request.Builder request = new Request.Builder().url("http://www.google.com");
FhirRequestBuilder.addResourceFormatHeaders(request, testFormat);
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET);
Map<String, List<String>> headersMap = request.build().headers().toMultimap();
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
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 + "\".");
Assertions.assertNull(headersMap.get("Content-Type"), "Content-Type header null.");
}
@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";
@DisplayName("Test resource format headers are added correctly (POST).")
void addResourceFormatHeadersPOST() {
String testFormat = "yaml";
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST);
Headers headers = new Headers.Builder().add(headerName1, headerValue1).add(headerName2, headerValue2).build();
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Request.Builder request = new Request.Builder().url("http://www.google.com");
FhirRequestBuilder.addHeaders(request, headers);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
Assertions.assertNotNull(headersMap.get("Accept"), "Accept header null.");
Assertions.assertEquals(testFormat, headersMap.get("Accept").get(0),
"Accept header not populated with expected value " + testFormat + ".");
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 + ".");
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
@ -115,7 +95,7 @@ class FhirRequestBuilderTest {
@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();
Iterable<HTTPHeader> headers = List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader));
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@ -123,8 +103,7 @@ class FhirRequestBuilderTest {
@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();
Iterable<HTTPHeader> headers = List.of(new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader));
Assertions.assertEquals(expectedContentLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@ -133,15 +112,17 @@ class FhirRequestBuilderTest {
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();
Iterable<HTTPHeader> headers = List.of(
new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader),
new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)
);
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));
Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList()));
}
}

View File

@ -126,6 +126,12 @@
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>

View File

@ -604,7 +604,7 @@ public class FHIRPathEngine {
warnings.addAll(typeWarnings);
return res;
}
public TypeDetails checkOnTypes(Object appContext, String resourceType, TypeDetails types, ExpressionNode expr, List<IssueMessage> warnings) throws FHIRLexerException, PathEngineException, DefinitionException {
typeWarnings.clear();
TypeDetails res = executeType(new ExecutionTypeContext(appContext, resourceType, types, types), types, expr, null, true, false, expr);
@ -612,6 +612,13 @@ public class FHIRPathEngine {
return res;
}
public TypeDetails checkOnTypes(Object appContext, String resourceType, TypeDetails types, ExpressionNode expr, List<IssueMessage> warnings, boolean canBeNone) throws FHIRLexerException, PathEngineException, DefinitionException {
typeWarnings.clear();
TypeDetails res = executeType(new ExecutionTypeContext(appContext, resourceType, types, types), types, expr, null, true, canBeNone, expr);
warnings.addAll(typeWarnings);
return res;
}
/**
* check that paths referred to in the ExpressionNode are valid
*
@ -6585,7 +6592,7 @@ public class FHIRPathEngine {
/** given an element definition in a profile, what element contains the differentiating fixed
* for the element, given the differentiating expresssion. The expression is only allowed to
* for the element, given the differentiating expression. The expression is only allowed to
* use a subset of FHIRPath
*
* @param profile

View File

@ -1,22 +1,14 @@
package org.hl7.fhir.r5.terminologies;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -156,9 +148,9 @@ public class TerminologyCacheManager {
// post it to
String url = "https://tx.fhir.org/post/tx-cache/"+ghOrg+"/"+ghRepo+"/"+ghBranch+".zip";
System.out.println("Sending tx-cache to "+url+" ("+Utilities.describeSize(bs.toByteArray().length)+")");
HTTPResult res = ManagedWebAccess.builder()
HTTPResult res = ManagedWebAccess.accessor()
.withBasicAuth(token.substring(0, token.indexOf(':')), token.substring(token.indexOf(':') + 1))
.withAccept("application/zip").put(url, bs.toByteArray(), null);
.put(url, bs.toByteArray(), null, "application/zip");
if (res.getCode() >= 300) {
System.out.println("sending cache failed: "+res.getCode());

View File

@ -2,7 +2,6 @@ package org.hl7.fhir.r5.terminologies.client;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/*
@ -41,13 +40,10 @@ 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;
import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.client.ITerminologyClient;
import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager.ITerminologyClientFactory;
import org.hl7.fhir.r5.terminologies.client.TerminologyClientR5.TerminologyClientR5Factory;
import org.hl7.fhir.r5.utils.client.FHIRToolingClient;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.FhirPublication;

View File

@ -1,12 +1,11 @@
package org.hl7.fhir.r5.utils.client;
import okhttp3.Headers;
import okhttp3.internal.http2.Header;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.exceptions.FHIRException;
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.client.EFhirClientException;
/*
Copyright (c) 2011+, HL7, Inc.
@ -45,7 +44,7 @@ import org.hl7.fhir.r5.utils.client.network.ResourceRequest;
import org.hl7.fhir.utilities.FHIRBaseToolingClient;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -93,18 +92,24 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
private String base;
private ResourceAddress resourceAddress;
@Setter
private ResourceFormat preferredResourceFormat;
private int maxResultSetSize = -1;//_count
private CapabilityStatement capabilities;
@Getter
@Setter
private Client client = new Client();
private ArrayList<Header> headers = new ArrayList<>();
private String username;
private String password;
private List<HTTPHeader> headers = new ArrayList<>();
@Setter
@Getter
private String userAgent;
@Setter
private String acceptLanguage;
private String acceptLang;
private String contentLang;
@Setter
private String contentLanguage;
private int useCount;
@ -124,22 +129,10 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
this.maxResultSetSize = -1;
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
public String getPreferredResourceFormat() {
return preferredResourceFormat.getHeader();
}
public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
preferredResourceFormat = resourceFormat;
}
public int getMaximumRecordCount() {
return maxResultSetSize;
}
@ -410,10 +403,14 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
}
/**
* Helper method to prevent nesting of previously thrown EFhirClientExceptions
* Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of
* EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the
* cause.
*
* @param e
* @throws EFhirClientException
* @param code The EFhirClientException code.
* @param message The EFhirClientException message.
* @param e The exception.
* @throws EFhirClientException representing the exception.
*/
protected void handleException(int code, String message, Exception e) throws EFhirClientException {
if (e instanceof EFhirClientException) {
@ -427,8 +424,8 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
* Helper method to determine whether desired resource representation
* is Json or XML.
*
* @param format
* @return
* @param format The format
* @return true if the format is JSON, false otherwise
*/
protected boolean isJson(String format) {
boolean isJson = false;
@ -573,22 +570,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return result == null ? null : (ConceptMap) result.getPayload();
}
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 long getTimeout() {
return client.getTimeout();
}
@ -613,51 +594,27 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
client.setRetryCount(retryCount);
}
public void setClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
public void setClientHeaders(Iterable<HTTPHeader> headers) {
this.headers = new ArrayList<>();
headers.forEach(this.headers::add);
}
private Headers generateHeaders(boolean hasBody) {
Headers.Builder builder = new Headers.Builder();
// Add basic auth header if it exists
if (basicAuthHeaderExists()) {
builder.add(getAuthorizationHeader().toString());
}
private Iterable<HTTPHeader> generateHeaders(boolean hasBody) {
// Add any other headers
if(this.headers != null) {
this.headers.forEach(header -> builder.add(header.toString()));
}
List<HTTPHeader> headers = new ArrayList<>(this.headers);
if (!Utilities.noString(userAgent)) {
builder.add("User-Agent: "+userAgent);
headers.add(new HTTPHeader("User-Agent",userAgent));
}
if (!Utilities.noString(acceptLang)) {
builder.add("Accept-Language: "+acceptLang);
if (!Utilities.noString(acceptLanguage)) {
headers.add(new HTTPHeader("Accept-Language", acceptLanguage));
}
if (hasBody && !Utilities.noString(contentLang)) {
builder.add("Content-Language: "+contentLang);
if (hasBody && !Utilities.noString(contentLanguage)) {
headers.add(new HTTPHeader("Content-Language", contentLanguage));
}
return builder.build();
}
public boolean basicAuthHeaderExists() {
return (username != null) && (password != null);
}
public Header getAuthorizationHeader() {
String usernamePassword = username + ":" + password;
String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
return new Header("Authorization", "Basic " + base64usernamePassword);
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
return headers;
}
public String getServerVersion() {
@ -671,14 +628,6 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
return capabilities == null ? null : capabilities.getSoftware().getVersion();
}
public void setAcceptLanguage(String lang) {
this.acceptLang = lang;
}
public void setContentLanguage(String lang) {
this.contentLang = lang;
}
public Bundle search(String type, String criteria) {
recordUse();
return fetchFeed(Utilities.pathURL(base, type+criteria));

View File

@ -1,16 +1,18 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import lombok.Getter;
import lombok.Setter;
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 org.hl7.fhir.utilities.http.HTTPRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -18,93 +20,60 @@ public class Client {
public static final String DEFAULT_CHARSET = "UTF-8";
private static final long DEFAULT_TIMEOUT = 5000;
@Getter @Setter
private ToolingClientLogger logger;
private FhirLoggingInterceptor fhirLoggingInterceptor;
@Setter @Getter
private int retryCount;
@Setter @Getter
private long timeout = DEFAULT_TIMEOUT;
private byte[] payload;
@Setter @Getter
private String base;
public String getBase() {
return base;
}
public void setBase(String base) {
this.base = base;
}
public ToolingClientLogger getLogger() {
return logger;
}
public void setLogger(ToolingClientLogger logger) {
this.logger = logger;
this.fhirLoggingInterceptor = new FhirLoggingInterceptor(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 {
this.payload = null;
Request.Builder request = new Request.Builder()
.method("OPTIONS", null)
.url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(optionsUri.toURL())
.withMethod(HTTPRequest.HttpMethod.OPTIONS);
return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> headers,
String message,
long timeout) throws IOException {
this.payload = null;
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public int tester(int trytry) {
return 5;
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException(0, "PUT requests require a non-null payload");
this.payload = payload;
RequestBody body = RequestBody.create(payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.put(body);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.PUT)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
@ -114,37 +83,38 @@ public class Client {
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
return issuePostRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException(0, "POST requests require a non-null payload");
this.payload = payload;
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
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, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest();
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.DELETE);
return executeFhirRequest(request, null, Collections.emptyList(), 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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.GET);
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle issuePostFeedRequest(URI resourceUri,
@ -154,57 +124,63 @@ public class Client {
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, new Headers.Builder().build(), null, retryCount, timeout);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> headers,
String message,
int timeout) throws IOException {
if (payload == null) throw new EFhirClientException(0, "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);
HTTPRequest request = new HTTPRequest()
.withUrl(resourceUri.toURL())
.withMethod(HTTPRequest.HttpMethod.POST)
.withBody(payload)
.withContentType(getContentTypeWithDefaultCharset(resourceFormat));
return executeBundleRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
private static String getContentTypeWithDefaultCharset(String resourceFormat) {
return resourceFormat + ";charset=" + DEFAULT_CHARSET;
}
public <T extends Resource> Bundle executeBundleRequest(HTTPRequest request,
String resourceFormat,
Iterable<HTTPHeader> headers,
String message,
int retryCount,
long timeout) throws IOException {
return new FhirRequestBuilder(request, base)
.withLogger(fhirLoggingInterceptor)
.withLogger(logger)
.withResourceFormat(resourceFormat)
.withRetryCount(retryCount)
.withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS)
.executeAsBatch();
}
public <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
public <T extends Resource> ResourceRequest<T> executeFhirRequest(HTTPRequest request,
String resourceFormat,
Iterable<HTTPHeader> headers,
String message,
int retryCount,
long timeout) throws IOException {
return new FhirRequestBuilder(request, base)
.withLogger(fhirLoggingInterceptor)
.withLogger(logger)
.withResourceFormat(resourceFormat)
.withRetryCount(retryCount)
.withMessage(message)
.withHeaders(headers == null ? new Headers.Builder().build() : headers)
.withHeaders(headers == null ? Collections.emptyList() : headers)
.withTimeout(timeout, TimeUnit.MILLISECONDS)
.execute();
}

View File

@ -1,7 +1,8 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.http.HTTPHeader;
import java.util.ArrayList;
import java.util.List;
@ -15,30 +16,31 @@ import java.util.stream.Collectors;
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
private final List<HTTPHeader> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
public ClientHeaders(List<HTTPHeader> headers) {
this.headers = new ArrayList<>(headers);
}
public ArrayList<Header> headers() {
public List<HTTPHeader> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @param header {@link HTTPHeader} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
public ClientHeaders addHeader(HTTPHeader header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", "
+ header.value + ">.");
throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", "
+ header.getValue() + ">.");
}
headers.add(header);
return this;
@ -47,39 +49,39 @@ public class ClientHeaders {
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @param headerList {@link List} of {@link HTTPHeader} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders addHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
/**
* Removes the passed in header from the list of stored headers.
* @param header {@link Header} to remove from the list.
* @param header {@link HTTPHeader} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", "
+ header.value + ">, from GenericClientHeaders that is not currently stored.");
throw new FHIRException("Attempting to remove header, <" + header.getName() + ", "
+ header.getValue() + ">, from GenericClientHeaders that is not currently stored.");
}
return this;
}
/**
* Removes the passed in headers from the list of stored headers.
* @param headerList {@link List} of {@link Header} to remove.
* @param headerList {@link List} of {@link HTTPHeader} to remove.
* @throws FHIRException if any of the headers passed in does not exist within the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
public ClientHeaders removeHeaders(List<HTTPHeader> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
* Clears all stored {@link HTTPHeader}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
@ -89,7 +91,7 @@ public class ClientHeaders {
@Override
public String toString() {
return this.headers.stream()
.map(header -> "\t" + header.name + ":" + header.value)
.map(header -> "\t" + header.getName() + ":" + header.getValue())
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -1,70 +0,0 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.*;
import okio.Buffer;
import org.hl7.fhir.utilities.ToolingClientLogger;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class FhirLoggingInterceptor implements Interceptor {
private ToolingClientLogger logger;
public FhirLoggingInterceptor(ToolingClientLogger logger) {
this.logger = logger;
}
public FhirLoggingInterceptor setLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@Override
public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException {
// Log Request
Request request = chain.request();
List<String> hdrs = new ArrayList<>();
for (String s : request.headers().toString().split("\\n")) {
hdrs.add(s.trim());
}
byte[] cnt = null;
if (request.body() != null) {
Buffer buf = new Buffer();
request.body().writeTo(buf);
cnt = buf.readByteArray();
}
if (logger != null) {
logger.logRequest(request.method(), request.url().toString(), hdrs, cnt);
}
// Log Response
Response response = null;
response = chain.proceed(chain.request());
MediaType contentType = null;
byte[] bodyBytes = null;
if (response.body() != null) {
contentType = response.body().contentType();
bodyBytes = response.body().bytes();
}
// Get Headers as List
List<String> headerList = new ArrayList<>();
Map<String, List<String>> headerMap = response.headers().toMultimap();
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
if (logger != null) {
long responseTimeInMillis = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, responseTimeInMillis);
}
// Reading byte[] clears body. Need to recreate.
ResponseBody body = ResponseBody.create(bodyBytes, contentType);
return response.newBuilder().body(body).build();
}
}

View File

@ -1,14 +1,12 @@
package org.hl7.fhir.r5.utils.client.network;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.formats.IParser;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.formats.XmlParser;
@ -20,32 +18,20 @@ 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.MimeType;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.http.*;
import org.hl7.fhir.utilities.xhtml.XhtmlUtils;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
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 DEFAULT_CHARSET = "UTF-8";
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 final HTTPRequest httpRequest;
private String resourceFormat = null;
private Headers headers = null;
private Iterable<HTTPHeader> headers = null;
private String message = null;
private int retryCount = 1;
/**
@ -58,69 +44,57 @@ public class FhirRequestBuilder {
private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
/**
* {@link FhirLoggingInterceptor} for log output.
* {@link ToolingClientLogger} for log output.
*/
private FhirLoggingInterceptor logger = null;
private ToolingClientLogger logger = null;
private String source;
public FhirRequestBuilder(Request.Builder httpRequest, String source) {
this.httpRequest = httpRequest;
public FhirRequestBuilder(HTTPRequest httpRequest, String source) {
this.source = source;
this.httpRequest = httpRequest;
}
/**
* Adds necessary default headers, formatting headers, and any passed in {@link Headers} to the passed in
* {@link okhttp3.Request.Builder}
* Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s to the passed in
* {@link HTTPRequest}
*
* @param request {@link okhttp3.Request.Builder} to add headers to.
* @param request {@link HTTPRequest} to add headers to.
* @param format Expected {@link Resource} format.
* @param headers Any additional {@link Headers} to add to the request.
* @param headers Any additional {@link HTTPHeader}s to add to the request.
*/
protected static void formatHeaders(Request.Builder request, String format, Headers headers) {
addDefaultHeaders(request, headers);
if (format != null) addResourceFormatHeaders(request, format);
if (headers != null) addHeaders(request, headers);
}
protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable<HTTPHeader> headers) {
List<HTTPHeader> allHeaders = new ArrayList<>();
request.getHeaders().forEach(allHeaders::add);
/**
* Adds necessary headers for all REST requests.
* <li>User-Agent : hapi-fhir-tooling-client</li>
*
* @param request {@link Request.Builder} to add default headers to.
*/
protected static void addDefaultHeaders(Request.Builder request, Headers headers) {
if (headers == null || !headers.names().contains("User-Agent")) {
request.addHeader("User-Agent", "hapi-fhir-tooling-client");
}
if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add);
if (headers != null) headers.forEach(allHeaders::add);
return request.withHeaders(allHeaders);
}
/**
* Adds necessary headers for the given resource format provided.
*
* @param request {@link Request.Builder} to add default headers to.
* @param httpRequest {@link HTTPRequest} to add default headers to.
* @param format Expected {@link Resource} format.
*/
protected static void addResourceFormatHeaders(Request.Builder request, String format) {
request.addHeader("Accept", format);
if (Utilities.existsInList(request.getMethod$okhttp(), "POST", "PUT")) {
request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET);
protected static Iterable<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
List<HTTPHeader> headers = new ArrayList<>();
headers.add(new HTTPHeader("Accept", format));
if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
|| httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
) {
headers.add( new HTTPHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET));
}
return headers;
}
/**
* 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}
* Returns true if any of the {@link OperationOutcome.OperationOutcomeIssueComponent} within the
* provided {@link OperationOutcome} have an {@link OperationOutcome.IssueSeverity} of
* {@link OperationOutcome.IssueSeverity#ERROR} or
* {@link OperationOutcome.IssueSeverity#FATAL}
*
* @param oo {@link OperationOutcome} to evaluate.
* @return {@link Boolean#TRUE} if an error exists.
@ -131,71 +105,8 @@ public class FhirRequestBuilder {
|| 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 (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
Authenticator proxyAuthenticator = getAuthenticator();
OkHttpClient.Builder builder = okHttpClient.newBuilder();
if (logger != null) builder.addInterceptor(logger);
builder.addInterceptor(new RetryInterceptor(retryCount));
return builder.connectTimeout(timeout, timeoutUnit)
.writeTimeout(timeout, timeoutUnit)
.readTimeout(timeout, timeoutUnit)
.proxyAuthenticator(proxyAuthenticator)
.build();
}
@Nonnull
private static Authenticator getAuthenticator() {
return (route, response) -> {
final String httpProxyUser = System.getProperty(HTTP_PROXY_USER);
final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS);
if (httpProxyUser != null && httpProxyPass != null) {
String credential = Credentials.basic(httpProxyUser, httpProxyPass);
return response.request().newBuilder()
.header(HEADER_PROXY_AUTH, credential)
.build();
}
return response.request().newBuilder().build();
};
protected ManagedFhirWebAccessor getManagedWebAccessor() {
return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
}
public FhirRequestBuilder withResourceFormat(String resourceFormat) {
@ -203,7 +114,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withHeaders(Headers headers) {
public FhirRequestBuilder withHeaders(Iterable<HTTPHeader> headers) {
this.headers = headers;
return this;
}
@ -218,7 +129,7 @@ public class FhirRequestBuilder {
return this;
}
public FhirRequestBuilder withLogger(FhirLoggingInterceptor logger) {
public FhirRequestBuilder withLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@ -229,20 +140,16 @@ public class FhirRequestBuilder {
return this;
}
protected Request buildRequest() {
return httpRequest.build();
}
public <T extends Resource> ResourceRequest<T> execute() throws IOException {
formatHeaders(httpRequest, resourceFormat, headers);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);//getHttpClient().newCall(httpRequest.build()).execute();
T resource = unmarshalReference(response, resourceFormat, null);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null);
HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
return unmarshalFeed(response, resourceFormat);
}
@ -250,12 +157,12 @@ public class FhirRequestBuilder {
* Unmarshalls a resource from the response stream.
*/
@SuppressWarnings("unchecked")
protected <T extends Resource> T unmarshalReference(Response response, String format, String resourceType) {
int code = response.code();
protected <T extends Resource> T unmarshalReference(HTTPResult response, String format, String resourceType) {
int code = response.getCode();
boolean ok = code >= 200 && code < 300;
if (response.body() == null) {
if (response.getContent() == null) {
if (!ok) {
throw new EFhirClientException(code, response.message());
throw new EFhirClientException(code, response.getMessage());
} else {
return null;
}
@ -264,9 +171,9 @@ public class FhirRequestBuilder {
Resource resource = null;
try {
body = response.body().string();
String ct = response.header("Content-Type");
if (ct == null) {
body = response.getContentAsString();
String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
if (contentType == null) {
if (ok) {
resource = getParser(format).parse(body);
} else {
@ -275,10 +182,10 @@ public class FhirRequestBuilder {
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
}
} else {
if (ct.contains(";")) {
ct = ct.substring(0, ct.indexOf(";"));
if (contentType.contains(";")) {
contentType = contentType.substring(0, contentType.indexOf(";"));
}
switch (ct) {
switch (contentType) {
case "application/json":
case "application/fhir+json":
if (!format.contains("json")) {
@ -298,10 +205,10 @@ public class FhirRequestBuilder {
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
break;
case "text/html" :
resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.body().string(), source));
resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source));
break;
default: // not sure what else to do?
System.out.println("Got content-type '"+ct+"' from "+source);
System.out.println("Got content-type '"+contentType+"' from "+source);
System.out.println(body);
resource = OperationOutcomeUtilities.outcomeFromTextError(body);
}
@ -336,14 +243,14 @@ public class FhirRequestBuilder {
/**
* Unmarshalls Bundle from response stream.
*/
protected Bundle unmarshalFeed(Response response, String format) {
protected Bundle unmarshalFeed(HTTPResult response, String format) {
return unmarshalReference(response, format, "Bundle");
}
/**
* Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is
* provided...because reasons.
* <p>
* <p/>
* Currently supports only "json" and "xml" formats.
*
* @param format One of "json" or "xml".
@ -362,4 +269,19 @@ public class FhirRequestBuilder {
throw new EFhirClientException(0, "Invalid format: " + format);
}
}
/**
* Extracts the 'location' header from the passed headers. If no value for 'location' exists, the
* value for 'content-location' is returned. If neither header exists, we return null.
*
* @param headers Headers to search for 'location' or 'content-location'.
*/
protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
if (locationHeader != null) {
return locationHeader;
}
return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
}
}

View File

@ -1,63 +0,0 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import javax.annotation.Nonnull;
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 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

@ -5,6 +5,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
@ -22,17 +23,14 @@ import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.utils.client.network.Client;
import org.hl7.fhir.r5.utils.client.network.ResourceRequest;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.*;
import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.internal.http2.Header;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.times;
@ -41,46 +39,56 @@ class FHIRToolingClientTest {
String TX_ADDR = FhirSettings.getTxFhirDevelopment();
Header h1 = new Header("header1", "value1");
Header h2 = new Header("header2", "value2");
Header h3 = new Header("header3", "value3");
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases");
private Client mockClient;
private FHIRToolingClient toolingClient;
@Captor
private ArgumentCaptor<Iterable<HTTPHeader>> headersArgumentCaptor;
@BeforeEach
void setUp() throws IOException, URISyntaxException {
MockitoAnnotations.openMocks(this);
mockClient = Mockito.mock(Client.class);
ResourceRequest<Resource> resourceResourceRequest = new ResourceRequest<>(generateBundle(), 200, "");
//GET
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong()))
Mockito.any(Iterable.class), Mockito.anyString(), Mockito.anyLong()))
.thenReturn(resourceResourceRequest);
//PUT
Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong()))
.thenReturn(resourceResourceRequest);
//POST
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong()))
.thenReturn(resourceResourceRequest);
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.contains("validate"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location"));
//BUNDLE REQ
Mockito.when(mockClient.executeBundleRequest(Mockito.any(Request.Builder.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
Mockito.when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(),
ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
.thenReturn(generateBundle());
toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases");
toolingClient.setClient(mockClient);
}
private ArrayList<Header> getHeaders() {
private List<HTTPHeader> getHeaders() {
return new ArrayList<>(Arrays.asList(h1, h2, h3));
}
private List<HTTPHeader> getHeadersWithAgent() {
return new ArrayList<>(Arrays.asList(h1, h2, h3, agentHeader));
}
private Bundle generateBundle() {
Patient patient = generatePatient();
Observation observation = generateObservation();
@ -140,36 +148,37 @@ class FHIRToolingClientTest {
return observation;
}
private void checkHeaders(Headers argumentCaptorValue) {
getHeaders().forEach(header -> {
// System.out.println("Checking header <" + header.component1().utf8() + ", " + header.component2().utf8() + ">");
Assertions.assertEquals(argumentCaptorValue.get(header.component1().utf8()), header.component2().utf8());
private void checkHeaders(Iterable<HTTPHeader> argumentCaptorValue) {
List<HTTPHeader> capturedHeaders = new ArrayList<>();
argumentCaptorValue.forEach(capturedHeaders::add);
getHeadersWithAgent().forEach(header -> {
assertTrue(capturedHeaders.contains(header));
});
}
@Test
void getTerminologyCapabilities() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location"));
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getTerminologyCapabilitiesNotSupported() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
Exception exception = assertThrows(FHIRException.class, () -> {
toolingClient.getTerminologyCapabilities();
@ -180,24 +189,23 @@ class FHIRToolingClientTest {
@Test
void getTerminologyCapabilitiesFailsForJSON() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenThrow(new FHIRFormatError("dummy error"))
.thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location"));
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClient, times(2)).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getTerminologyCapabilitiesStatementFailsForJSONandXML() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenThrow(new FHIRFormatError("dummy error"))
.thenThrow(new FHIRFormatError("dummy error 2"));
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
@ -210,10 +218,9 @@ class FHIRToolingClientTest {
@Test
void getCapabilitiesStatement() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
toolingClient.setClientHeaders(getHeaders());
@ -222,18 +229,17 @@ class FHIRToolingClientTest {
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatementFailsForJSON() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenThrow(new FHIRFormatError("dummy error"))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatement();
@ -241,14 +247,14 @@ class FHIRToolingClientTest {
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
assertEquals(ResourceFormat.RESOURCE_XML.getHeader(), toolingClient.getPreferredResourceFormat());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatementFailsForJSONandXML() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenThrow(new FHIRFormatError("dummy error"))
.thenThrow(new FHIRFormatError("dummy error 2"));
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
@ -261,17 +267,16 @@ class FHIRToolingClientTest {
@Test
void getCapabilitiesStatementQuick() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatementQuick();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
@ -280,19 +285,18 @@ class FHIRToolingClientTest {
@Test
void getCapabilitiesStatementQuickFailsForJSON() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenThrow(new FHIRFormatError("dummy error"))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatementQuick();
Mockito.verify(mockClient, times(2)).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
assertEquals(ResourceFormat.RESOURCE_XML.getHeader(), toolingClient.getPreferredResourceFormat());
@ -301,11 +305,10 @@ class FHIRToolingClientTest {
@Test
void getCapabilitiesStatementQuickFailsForJSONandXML() throws IOException {
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenThrow(new FHIRFormatError("dummy error"))
.thenThrow(new FHIRFormatError("dummy error 2"));
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat());
Exception exception = assertThrows(FHIRException.class, () -> {
@ -317,63 +320,58 @@ class FHIRToolingClientTest {
@Test
void read() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.read(Patient.class, "id");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void vread() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.vread(Patient.class, "id", "version");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCanonical() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCanonical(Patient.class, "canonicalURL");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void update() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.update(generatePatient());
Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void validate() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.validate(Patient.class, generatePatient(), "id");
Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
Iterable<HTTPHeader> argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
}

View File

@ -4,20 +4,19 @@ import java.util.Arrays;
import java.util.List;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import okhttp3.internal.http2.Header;
class ClientHeadersTest {
ClientHeaders clientHeaders;
Header h1 = new Header("header1", "value1");
Header h2 = new Header("header2", "value2");
Header h3 = new Header("header3", "value3");
HTTPHeader h1 = new HTTPHeader("header1", "value1");
HTTPHeader h2 = new HTTPHeader("header2", "value2");
HTTPHeader h3 = new HTTPHeader("header3", "value3");
@BeforeEach
void setUp() {
@ -43,7 +42,7 @@ class ClientHeadersTest {
@Test
@DisplayName("Happy path add headers as list.")
void addHeaders() {
List<Header> headersList = Arrays.asList(h1, h2, h3);
List<HTTPHeader> headersList = Arrays.asList(h1, h2, h3);
clientHeaders.addHeaders(headersList);
Assertions.assertEquals(3, clientHeaders.headers().size());
Assertions.assertEquals(headersList, clientHeaders.headers());
@ -52,7 +51,7 @@ class ClientHeadersTest {
@Test
@DisplayName("Happy path add headers as list.")
void addHeadersDuplicateAdd() {
List<Header> headersList = Arrays.asList(h1, h2, h1);
List<HTTPHeader> headersList = Arrays.asList(h1, h2, h1);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList));
}
@ -64,7 +63,7 @@ class ClientHeadersTest {
clientHeaders.addHeader(h3);
clientHeaders.removeHeader(h2);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.removeHeader(new Header("header3", "value3"));
clientHeaders.removeHeader(new HTTPHeader("header3", "value3"));
Assertions.assertEquals(1, clientHeaders.headers().size());
}
@ -79,8 +78,8 @@ class ClientHeadersTest {
@Test
@DisplayName("Happy path remove list of existing headers.")
void removeHeaders() {
List<Header> headersToAddList = Arrays.asList(h1, h2, h3);
List<Header> headersToRemoveList = Arrays.asList(h2, h3);
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h2, h3);
List<HTTPHeader> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
clientHeaders.removeHeaders(headersToRemoveList);
Assertions.assertEquals(1, clientHeaders.headers().size());
@ -89,15 +88,15 @@ class ClientHeadersTest {
@Test
@DisplayName("Remove list containing unknown header.")
void removeHeadersUnknown() {
List<Header> headersToAddList = Arrays.asList(h1, h3);
List<Header> headersToRemoveList = Arrays.asList(h2, h3);
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h3);
List<HTTPHeader> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList));
}
@Test
void clearHeaders() {
List<Header> headersToAddList = Arrays.asList(h1, h3);
List<HTTPHeader> headersToAddList = Arrays.asList(h1, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.clearHeaders();

View File

@ -1,39 +1,29 @@
package org.hl7.fhir.r5.utils.client.network;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.r5.model.OperationOutcome;
import org.hl7.fhir.utilities.http.HTTPHeaderUtil;
import org.hl7.fhir.utilities.http.HTTPRequest;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.http.HTTPResult;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import okhttp3.Headers;
import okhttp3.Request;
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, null);
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\".");
}
@Test
@DisplayName("Test resource format headers are added correctly (GET).")
void addResourceFormatHeadersGET() {
String testFormat = "yaml";
Request.Builder request = new Request.Builder().url("http://www.google.com");
request.setMethod$okhttp("GET");
FhirRequestBuilder.addResourceFormatHeaders(request, testFormat);
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET);
Map<String, List<String>> headersMap = request.build().headers().toMultimap();
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
Assertions.assertNotNull(headersMap.get("Accept"), "Accept header null.");
Assertions.assertEquals(testFormat, headersMap.get("Accept").get(0),
"Accept header not populated with expected value " + testFormat + ".");
@ -45,11 +35,11 @@ class FhirRequestBuilderTest {
@DisplayName("Test resource format headers are added correctly (POST).")
void addResourceFormatHeadersPOST() {
String testFormat = "yaml";
Request.Builder request = new Request.Builder().url("http://www.google.com");
request.setMethod$okhttp("POST");
FhirRequestBuilder.addResourceFormatHeaders(request, testFormat);
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST);
Map<String, List<String>> headersMap = request.build().headers().toMultimap();
Iterable<HTTPHeader> headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
Map<String, List<String>> headersMap = HTTPHeaderUtil.getMultimap(headers);
Assertions.assertNotNull(headersMap.get("Accept"), "Accept header null.");
Assertions.assertEquals(testFormat, headersMap.get("Accept").get(0),
"Accept header not populated with expected value " + testFormat + ".");
@ -59,31 +49,6 @@ class FhirRequestBuilderTest {
"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() {
@ -120,19 +85,22 @@ class FhirRequestBuilderTest {
@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));
HTTPResult result = new HTTPResult("source",
200,
"message",
"contentType",
new byte[0],
List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader)));
Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(result.getHeaders()));
}
@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();
Iterable<HTTPHeader> headers = List.of(new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader));
Assertions.assertEquals(expectedContentLocationHeader, FhirRequestBuilder.getLocationHeader(headers));
}
@ -141,18 +109,18 @@ class FhirRequestBuilderTest {
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();
Iterable<HTTPHeader> headers = List.of(
new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader),
new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)
);
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));
Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList()));
}
}

View File

@ -0,0 +1,21 @@
package org.hl7.fhir.utilities.http;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.With;
import javax.annotation.Nonnull;
@EqualsAndHashCode
public class HTTPHeader {
@With @Getter @Nonnull
private final String name;
@With @Getter
private final String value;
public HTTPHeader(@Nonnull String name, String value) {
this.name = name;
this.value = value;
}
}

View File

@ -0,0 +1,45 @@
package org.hl7.fhir.utilities.http;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HTTPHeaderUtil {
public static final String USER_AGENT = "User-Agent";
public static Map<String, List<String>> getMultimap(Iterable<HTTPHeader> headers) {
Map<String, List<String>> result = new HashMap<>();
if (headers != null) {
for (HTTPHeader header : headers) {
List<String> values = result.getOrDefault(header.getName(), new ArrayList<>());
values.add(header.getValue());
result.put(header.getName(), values);
}
}
return result;
}
public static Iterable<String> getHeaders(Iterable<HTTPHeader> headers, String key) {
List<String> result = new ArrayList<>();
if (headers != null) {
for (HTTPHeader header : headers) {
if (header.getName().equalsIgnoreCase(key)) {
result.add(header.getValue());
}
}
}
return result;
}
public static String getSingleHeader(Iterable<HTTPHeader> headers, String key) {
for (HTTPHeader header : headers) {
if (header.getName().equalsIgnoreCase(key)) {
return header.getValue();
}
}
return null;
}
}

View File

@ -0,0 +1,54 @@
package org.hl7.fhir.utilities.http;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.With;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
@AllArgsConstructor
public class HTTPRequest {
public HTTPRequest() {
url = null;
method = HttpMethod.GET;
body = null;
contentType = null;
headers = Collections.emptyList();
}
public enum HttpMethod {
GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH
}
@Getter @Nullable
private final URL url;
public HTTPRequest withUrl(URL url) {
return new HTTPRequest(url, method, body, contentType, headers);
}
public HTTPRequest withUrl(String url) {
try {
return withUrl(new URL(url));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid URL: " + url, e);
}
}
@With @Getter
private final HttpMethod method;
@With @Getter @Nullable
private final byte[] body;
@With @Getter @Nullable
private final String contentType;
@With @Getter @Nonnull
private final Iterable<HTTPHeader> headers;
}

View File

@ -2,39 +2,40 @@ package org.hl7.fhir.utilities.http;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import lombok.Getter;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;
public class HTTPResult {
private int code;
private String contentType;
private byte[] content;
private String source;
private String message;
@Getter
private final int code;
@Getter
private final String contentType;
@Getter
private final byte[] content;
@Getter
private final String source;
@Getter
private final String message;
@Getter
private final Iterable<HTTPHeader> headers;
public HTTPResult(String source, int code, String message, String contentType, byte[] content) {
this(source, code, message, contentType, content, Collections.emptyList());
}
public HTTPResult(String source, int code, String message, String contentType, byte[] content, Iterable<HTTPHeader> headers) {
super();
this.source = source;
this.code = code;
this.contentType = contentType;
this.content = content;
this.message = message;
}
public int getCode() {
return code;
}
public String getContentType() {
return contentType;
}
public byte[] getContent() {
return content;
}
public String getSource() {
return source;
this.headers = headers;
}
public void checkThrowException() throws IOException {
@ -52,11 +53,7 @@ public class HTTPResult {
}
}
public String getMessage() {
return message;
}
public String getContentAsString() {
return new String(content, StandardCharsets.UTF_8);
}
}
}

View File

@ -0,0 +1,170 @@
package org.hl7.fhir.utilities.http;
import okhttp3.*;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.http.okhttpimpl.LoggingInterceptor;
import org.hl7.fhir.utilities.http.okhttpimpl.ProxyAuthenticator;
import org.hl7.fhir.utilities.http.okhttpimpl.RetryInterceptor;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class ManagedFhirWebAccessor extends ManagedWebAccessorBase<ManagedFhirWebAccessor> {
/**
* The singleton instance of the HttpClient, used for all requests.
*/
private static OkHttpClient okHttpClient;
private long timeout;
private TimeUnit timeoutUnit;
private int retries;
private ToolingClientLogger logger;
private LoggingInterceptor loggingInterceptor;
public ManagedFhirWebAccessor withTimeout(long timeout, TimeUnit timeoutUnit) {
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
return this;
}
public ManagedFhirWebAccessor withRetries(int retries) {
this.retries = retries;
return this;
}
public ManagedFhirWebAccessor withLogger(ToolingClientLogger logger) {
this.logger = logger;
this.loggingInterceptor = new LoggingInterceptor(logger);
return this;
}
public ManagedFhirWebAccessor(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
super(userAgent, serverAuthDetails);
this.timeout = 5000;
this.timeoutUnit = TimeUnit.MILLISECONDS;
}
protected HTTPRequest httpRequestWithDefaultHeaders(HTTPRequest request) {
List<HTTPHeader> headers = new ArrayList<>();
if (HTTPHeaderUtil.getSingleHeader(request.getHeaders(), HTTPHeaderUtil.USER_AGENT) == null
&& getUserAgent() != null) {
headers.add(new HTTPHeader(HTTPHeaderUtil.USER_AGENT, getUserAgent()));
}
request.getHeaders().forEach(headers::add);
return request.withHeaders(headers);
}
protected HTTPRequest requestWithManagedHeaders(HTTPRequest httpRequest) {
HTTPRequest requestWithDefaultHeaders = httpRequestWithDefaultHeaders(httpRequest);
List<HTTPHeader> headers = new ArrayList<>();
requestWithDefaultHeaders.getHeaders().forEach(headers::add);
for (Map.Entry<String, String> entry : this.getHeaders().entrySet()) {
headers.add(new HTTPHeader(entry.getKey(), entry.getValue()));
}
if (getAuthenticationMode() != null) {
if (getAuthenticationMode() != HTTPAuthenticationMode.NONE) {
switch (getAuthenticationMode()) {
case BASIC:
final String basicCredential = Credentials.basic(getUsername(), getPassword());
headers.add(new HTTPHeader("Authorization", basicCredential));
break;
case TOKEN:
String tokenCredential = "Bearer " + getToken();
headers.add(new HTTPHeader("Authorization", tokenCredential));
break;
case APIKEY:
String apiKeyCredential = getToken();
headers.add(new HTTPHeader("Api-Key", apiKeyCredential));
break;
}
}
} else {
ServerDetailsPOJO settings = ManagedWebAccessUtils.getServer(httpRequest.getUrl().toString(), getServerAuthDetails());
if (settings != null) {
switch (settings.getAuthenticationType()) {
case "basic":
final String basicCredential = Credentials.basic(settings.getUsername(), settings.getPassword());
headers.add(new HTTPHeader("Authorization", basicCredential));
break;
case "token":
String tokenCredential = "Bearer " + settings.getToken();
headers.add(new HTTPHeader("Authorization", tokenCredential));
break;
case "apikey":
String apiKeyCredential = settings.getApikey();
headers.add(new HTTPHeader("Api-Key", apiKeyCredential));
break;
}
}
}
return httpRequest.withHeaders(headers);
}
public HTTPResult httpCall(HTTPRequest httpRequest) throws IOException {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
HTTPRequest httpRequestWithDirectHeaders = requestWithManagedHeaders(httpRequest);
assert httpRequestWithDirectHeaders.getUrl() != null;
RequestBody body = httpRequestWithDirectHeaders.getBody() == null ? null : RequestBody.create(httpRequestWithDirectHeaders.getBody());
Request.Builder requestBuilder = new Request.Builder()
.url(httpRequestWithDirectHeaders.getUrl())
.method(httpRequestWithDirectHeaders.getMethod().name(), body);
for (HTTPHeader header : httpRequestWithDirectHeaders.getHeaders()) {
requestBuilder.addHeader(header.getName(), header.getValue());
}
OkHttpClient okHttpClient = getOkHttpClient();
//TODO check and throw based on httpRequest:
if (!ManagedWebAccess.inAllowedPaths(httpRequestWithDirectHeaders.getUrl().toString())) {
throw new IOException("The pathname '"+httpRequestWithDirectHeaders.getUrl().toString()+"' cannot be accessed by policy");}
Response response = okHttpClient.newCall(requestBuilder.build()).execute();
return getHTTPResult(response);
case MANAGED:
HTTPRequest httpRequestWithManagedHeaders = requestWithManagedHeaders(httpRequest);
assert httpRequestWithManagedHeaders.getUrl() != null;
return ManagedWebAccess.getFhirWebAccessor().httpCall(httpRequestWithManagedHeaders);
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
throw new IOException("Internal Error");
}
}
private HTTPResult getHTTPResult(Response execute) throws IOException {
return new HTTPResult(execute.request().url().toString(), execute.code(), execute.message(), execute.header("Content-Type"), execute.body() != null && execute.body().contentLength() != 0 ? execute.body().bytes() : null, getHeadersFromResponse(execute));
}
private Iterable<HTTPHeader> getHeadersFromResponse(Response response) {
List<HTTPHeader> headers = new ArrayList<>();
for (String name : response.headers().names()) {
headers.add(new HTTPHeader(name, response.header(name)));
}
return headers;
}
private OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
OkHttpClient.Builder builder = okHttpClient.newBuilder();
if (logger != null) builder.addInterceptor(loggingInterceptor);
builder.addInterceptor(new RetryInterceptor(retries));
builder.proxyAuthenticator(new ProxyAuthenticator());
return builder.connectTimeout(timeout, timeoutUnit)
.writeTimeout(timeout, timeoutUnit)
.readTimeout(timeout, timeoutUnit).build();
}
}

View File

@ -31,23 +31,16 @@ package org.hl7.fhir.utilities.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import org.hl7.fhir.utilities.Utilities;
import lombok.Getter;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
/**
* see security.md - manages access to the local file system by the FHIR HAPI Core library
*
* see security.md - manages web access by the FHIR HAPI Core library
* <p/>
* By using accessPolicy, allowedDomains and accessor, a host java application can control
* whether this library has direct access to the web (and which domains it is allowed to access),
* or whether the host application provides controlled access, or whether no access is allowed at all
@ -57,13 +50,17 @@ import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
*
*/
public class ManagedWebAccess {
public interface IWebAccessor {
HTTPResult get(String url, String accept, Map<String, String> headers) throws IOException;
HTTPResult post(String url, byte[] bytes, String contentType, String accept, Map<String, String> headers) throws IOException;
HTTPResult put(String url, byte[] bytes, String contentType, String accept, Map<String, String> headers) throws IOException;
}
public interface IFhirWebAccessor {
HTTPResult httpCall(HTTPRequest httpRequest);
}
public enum WebAccessPolicy {
DIRECT, // open access to the web, though access can be restricted only to domains in AllowedDomains
MANAGED, // no access except by the IWebAccessor
@ -71,12 +68,18 @@ public class ManagedWebAccess {
}
private static WebAccessPolicy accessPolicy = WebAccessPolicy.DIRECT; // for legacy reasons
//TODO get this from fhir settings
private static List<String> allowedDomains = new ArrayList<>();
@Getter
private static IWebAccessor accessor;
@Getter
private static IFhirWebAccessor fhirWebAccessor;
@Getter
private static String userAgent;
private static List<ServerDetailsPOJO> serverAuthDetails;
public static WebAccessPolicy getAccessPolicy() {
return accessPolicy;
}
@ -97,37 +100,51 @@ public class ManagedWebAccess {
return false;
}
public static String getUserAgent() {
return userAgent;
}
public static void setUserAgent(String userAgent) {
ManagedWebAccess.userAgent = userAgent;
}
public static IWebAccessor getAccessor() {
return accessor;
public static ManagedWebAccessor accessor() {
return new ManagedWebAccessor(userAgent, serverAuthDetails);
}
public static ManagedWebAccessBuilder builder() {
return new ManagedWebAccessBuilder(userAgent, serverAuthDetails);
public static ManagedFhirWebAccessor fhirAccessor() {
return new ManagedFhirWebAccessor(userAgent, serverAuthDetails);
}
public static HTTPResult get(String url) throws IOException {
return builder().get(url);
return accessor().get(url);
}
public static HTTPResult get(String url, String accept) throws IOException {
return builder().withAccept(accept).get(url);
return accessor().get(url, accept);
}
public static HTTPResult post(String url, byte[] content, String contentType, String accept) throws IOException {
return builder().withAccept(accept).post(url, content, contentType);
return accessor().post(url, content, contentType, accept);
}
public static HTTPResult put(String url, byte[] content, String contentType, String accept) throws IOException {
return builder().withAccept(accept).put(url, content, contentType);
return accessor().put(url, content, contentType, accept);
}
public static HTTPResult httpCall(HTTPRequest httpRequest) throws IOException {
return fhirAccessor().httpCall(httpRequest);
}
public static void loadFromFHIRSettings() {
setAccessPolicy(FhirSettings.isProhibitNetworkAccess() ? WebAccessPolicy.PROHIBITED : WebAccessPolicy.DIRECT);
setUserAgent("hapi-fhir-tooling-client");
serverAuthDetails = new ArrayList<>();
serverAuthDetails.addAll(FhirSettings.getPackageServers());
serverAuthDetails.addAll(FhirSettings.getTerminologyServers());
}
public static void loadFromFHIRSettings(FhirSettings settings) {
setAccessPolicy(settings.isProhibitNetworkAccess() ? WebAccessPolicy.PROHIBITED : WebAccessPolicy.DIRECT);
setUserAgent("hapi-fhir-tooling-client");
serverAuthDetails = new ArrayList<>();
serverAuthDetails.addAll(settings.getPackageServers());
serverAuthDetails.addAll(settings.getTerminologyServers());
}
}

View File

@ -0,0 +1,17 @@
package org.hl7.fhir.utilities.http;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
public class ManagedWebAccessUtils {
public static ServerDetailsPOJO getServer(String url, Iterable<ServerDetailsPOJO> serverAuthDetails) {
if (serverAuthDetails != null) {
for (ServerDetailsPOJO t : serverAuthDetails) {
if (url.startsWith(t.getUrl())) {
return t;
}
}
}
return null;
}
}

View File

@ -9,61 +9,30 @@ import java.util.Map;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
/**
* Simple HTTP client for making requests to a server.
*/
public class ManagedWebAccessor extends ManagedWebAccessorBase<ManagedWebAccessor> {
public class ManagedWebAccessBuilder {
private String userAgent;
private HTTPAuthenticationMode authenticationMode;
private String username;
private String password;
private String token;
private String accept;
private List<ServerDetailsPOJO> serverAuthDetails;
private Map<String, String> headers = new HashMap<String, String>();
public ManagedWebAccessBuilder(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
this.userAgent = userAgent;
this.serverAuthDetails = serverAuthDetails;
}
public ManagedWebAccessBuilder withAccept(String accept) {
this.accept = accept;
return this;
}
public ManagedWebAccessBuilder withHeader(String name, String value) {
headers.put(name, value);
return this;
}
public ManagedWebAccessBuilder withBasicAuth(String username, String password) {
this.authenticationMode = HTTPAuthenticationMode.BASIC;
this.username = username;
this.password = password;
return this;
public ManagedWebAccessor(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
super(userAgent, serverAuthDetails);
}
public ManagedWebAccessBuilder withToken(String token) {
this.authenticationMode = HTTPAuthenticationMode.TOKEN;
this.token = token;
return this;
}
private Map<String, String> headers() {
Map<String, String> headers = new HashMap<String, String>();
headers.putAll(this.headers);
if (authenticationMode == HTTPAuthenticationMode.TOKEN) {
headers.put("Authorization", "Bearer " + token);
} else if (authenticationMode == HTTPAuthenticationMode.BASIC) {
String auth = username+":"+password;
private Map<String, String> newHeaders() {
Map<String, String> headers = new HashMap<String, String>(this.getHeaders());
if (getAuthenticationMode() == HTTPAuthenticationMode.TOKEN) {
headers.put("Authorization", "Bearer " + getToken());
} else if (getAuthenticationMode() == HTTPAuthenticationMode.BASIC) {
String auth = getUsername() +":"+ getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8));
headers.put("Authorization", "Basic " + new String(encodedAuth));
} else if (getAuthenticationMode() == HTTPAuthenticationMode.APIKEY) {
headers.put("Api-Key", getToken());
}
if (userAgent != null) {
headers.put("User-Agent", userAgent);
if (getUserAgent() != null) {
headers.put("User-Agent", getUserAgent());
}
return headers;
}
@ -72,25 +41,32 @@ public class ManagedWebAccessBuilder {
throw new IOException("The pathname '"+url+"' cannot be accessed by policy");
}
SimpleHTTPClient client = new SimpleHTTPClient();
if (userAgent != null) {
client.addHeader("User-Agent", userAgent);
for (Map.Entry<String, String> entry : this.getHeaders().entrySet()) {
client.addHeader(entry.getKey(), entry.getValue());
}
if (authenticationMode != null && authenticationMode != HTTPAuthenticationMode.NONE) {
client.setAuthenticationMode(authenticationMode);
switch (authenticationMode) {
if (getUserAgent() != null) {
client.addHeader("User-Agent", getUserAgent());
}
if (getAuthenticationMode() != null) {
if (getAuthenticationMode() != HTTPAuthenticationMode.NONE) {
client.setAuthenticationMode(getAuthenticationMode());
switch (getAuthenticationMode()) {
case BASIC :
client.setUsername(username);
client.setPassword(password);
client.setUsername(getUsername());
client.setPassword(getPassword());
break;
case TOKEN :
client.setToken(token);
client.setToken(getToken());
break;
case APIKEY :
client.setApiKey(token);
client.setApiKey(getToken());
break;
}
}
} else {
ServerDetailsPOJO settings = getServer(url);
ServerDetailsPOJO settings = ManagedWebAccessUtils.getServer(url, getServerAuthDetails());
if (settings != null) {
switch (settings.getAuthenticationType()) {
case "basic" :
@ -109,32 +85,23 @@ public class ManagedWebAccessBuilder {
}
}
}
if (username != null || token != null) {
client.setAuthenticationMode(authenticationMode);
if (getUsername() != null || getToken() != null) {
client.setAuthenticationMode(getAuthenticationMode());
}
return client;
}
private ServerDetailsPOJO getServer(String url) {
if (serverAuthDetails != null) {
for (ServerDetailsPOJO t : serverAuthDetails) {
if (url.startsWith(t.getUrl())) {
return t;
}
}
}
return null;
public HTTPResult get(String url) throws IOException {
return get(url, null);
}
public HTTPResult get(String url) throws IOException {
public HTTPResult get(String url, String accept) throws IOException {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
SimpleHTTPClient client = setupClient(url);
return client.get(url, accept);
case MANAGED:
return ManagedWebAccess.getAccessor().get(url, accept, headers());
return ManagedWebAccess.getAccessor().get(url, accept, newHeaders());
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
@ -142,14 +109,17 @@ public class ManagedWebAccessBuilder {
}
}
public HTTPResult post(String url, byte[] content, String contentType) throws IOException {
return post(url, content, contentType, null);
}
public HTTPResult post(String url, byte[] content, String contentType, String accept) throws IOException {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
SimpleHTTPClient client = setupClient(url);
return client.post(url, contentType, content, accept);
case MANAGED:
return ManagedWebAccess.getAccessor().post(url, content, contentType, accept, headers());
return ManagedWebAccess.getAccessor().post(url, content, contentType, accept, newHeaders());
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
@ -158,17 +128,20 @@ public class ManagedWebAccessBuilder {
}
public HTTPResult put(String url, byte[] content, String contentType) throws IOException {
return put(url, content, contentType, null);
}
public HTTPResult put(String url, byte[] content, String contentType, String accept) throws IOException {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
SimpleHTTPClient client = setupClient(url);
return client.put(url, contentType, content, accept);
case MANAGED:
return ManagedWebAccess.getAccessor().put(url, content, contentType, accept, headers());
return ManagedWebAccess.getAccessor().put(url, content, contentType, accept, newHeaders());
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
throw new IOException("Internal Error");
}
}
}

View File

@ -0,0 +1,78 @@
package org.hl7.fhir.utilities.http;
import lombok.Getter;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class ManagedWebAccessorBase<B extends ManagedWebAccessorBase<B>> {
@Getter
private final String userAgent;
@Getter
private HTTPAuthenticationMode authenticationMode;
@Getter
private String username;
@Getter
private String password;
@Getter
private String token;
@Getter
private final List<ServerDetailsPOJO> serverAuthDetails;
@Getter
private final Map<String, String> headers = new HashMap<>();
public ManagedWebAccessorBase(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
this.userAgent = userAgent;
this.serverAuthDetails = serverAuthDetails;
}
@SuppressWarnings("unchecked")
final B self() {
return (B) this;
}
public B withHeader(String name, String value) {
headers.put(name, value);
return self();
}
public B withBasicAuth(String username, String password) {
this.authenticationMode = HTTPAuthenticationMode.BASIC;
this.username = username;
this.password = password;
return self();
}
public B withToken(String token) {
this.authenticationMode = HTTPAuthenticationMode.TOKEN;
this.token = token;
return self();
}
public B withApiKey(String apiKey) {
this.authenticationMode = HTTPAuthenticationMode.APIKEY;
this.token = apiKey;
return self();
}
public B withNoneAuth() {
this.authenticationMode = HTTPAuthenticationMode.NONE;
setAllAuthHeadersToNull();
return self();
}
public B withServerSpecificAuth() {
this.authenticationMode = null;
setAllAuthHeadersToNull();
return self();
}
private void setAllAuthHeadersToNull() {
this.token = null;
this.username = null;
this.password = null;
}
}

View File

@ -20,27 +20,10 @@ import lombok.Setter;
public class SimpleHTTPClient {
public static class Header {
private String name;
private String value;
public Header(String name, String value) {
super();
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
private static final int MAX_REDIRECTS = 5;
private static int counter = 1;
private List<Header> headers = new ArrayList<>();
private List<HTTPHeader> headers = new ArrayList<>();
@Getter @Setter
private HTTPAuthenticationMode authenticationMode;
@ -58,7 +41,7 @@ public class SimpleHTTPClient {
private String apiKey;
public void addHeader(String name, String value) {
headers.add(new Header(name, value));
headers.add(new HTTPHeader(name, value));
}
public HTTPResult get(String url) throws IOException {
@ -114,8 +97,8 @@ public class SimpleHTTPClient {
private void setHeaders(HttpURLConnection c) {
if (headers != null) {
for (Header h : headers) {
c.setRequestProperty(h.getName(), h.getValue());
for (HTTPHeader header : headers) {
c.setRequestProperty(header.getName(), header.getValue());
}
}
c.setConnectTimeout(15000);
@ -126,7 +109,7 @@ public class SimpleHTTPClient {
private void setAuthenticationHeader(HttpURLConnection c) {
String authHeaderValue = null;
if (authenticationMode == HTTPAuthenticationMode.TOKEN) {
authHeaderValue = "Bearer " + new String(token);
authHeaderValue = "Bearer " + token;
} else if (authenticationMode == HTTPAuthenticationMode.BASIC) {
String auth = username+":"+password;
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8));

View File

@ -0,0 +1,69 @@
package org.hl7.fhir.utilities.http.okhttpimpl;
import okhttp3.*;
import okio.Buffer;
import org.hl7.fhir.utilities.ToolingClientLogger;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class LoggingInterceptor implements Interceptor{
private ToolingClientLogger logger;
public LoggingInterceptor(ToolingClientLogger logger) {
this.logger = logger;
}
public LoggingInterceptor setLogger(ToolingClientLogger logger) {
this.logger = logger;
return this;
}
@Override
public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException {
// Log Request
Request request = chain.request();
List<String> hdrs = new ArrayList<>();
for (String s : request.headers().toString().split("\\n")) {
hdrs.add(s.trim());
}
byte[] cnt = null;
if (request.body() != null) {
Buffer buf = new Buffer();
request.body().writeTo(buf);
cnt = buf.readByteArray();
}
if (logger != null) {
logger.logRequest(request.method(), request.url().toString(), hdrs, cnt);
}
// Log Response
Response response = null;
response = chain.proceed(chain.request());
MediaType contentType = null;
byte[] bodyBytes = null;
if (response.body() != null) {
contentType = response.body().contentType();
bodyBytes = response.body().bytes();
}
// Get Headers as List
List<String> headerList = new ArrayList<>();
Map<String, List<String>> headerMap = response.headers().toMultimap();
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
if (logger != null) {
long responseTimeInMillis = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, responseTimeInMillis);
}
// Reading byte[] clears body. Need to recreate.
ResponseBody body = ResponseBody.create(bodyBytes, contentType);
return response.newBuilder().body(body).build();
}
}

View File

@ -0,0 +1,24 @@
package org.hl7.fhir.utilities.http.okhttpimpl;
import okhttp3.*;
import java.io.IOException;
public class ProxyAuthenticator implements Authenticator {
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";
@Override
public Request authenticate(Route route, Response response) throws IOException {
final String httpProxyUser = System.getProperty(HTTP_PROXY_USER);
final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS);
if (httpProxyUser != null && httpProxyPass != null) {
String credential = Credentials.basic(httpProxyUser, httpProxyPass);
return response.request().newBuilder()
.header(HEADER_PROXY_AUTH, credential)
.build();
}
return response.request().newBuilder().build();
}
}

View File

@ -1,11 +1,11 @@
package org.hl7.fhir.dstu3.utils.client.network;
import java.io.IOException;
package org.hl7.fhir.utilities.http.okhttpimpl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
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.
@ -26,7 +26,7 @@ public class RetryInterceptor implements Interceptor {
}
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;

View File

@ -178,7 +178,6 @@ public class I18nConstants {
public static final String DIFFERENTIAL_WALKS_INTO____BUT_THE_BASE_DOES_NOT_AND_THERE_IS_NOT_A_SINGLE_FIXED_TYPE_THE_TYPE_IS__THIS_IS_NOT_HANDLED_YET = "Differential_walks_into____but_the_base_does_not_and_there_is_not_a_single_fixed_type_The_type_is__This_is_not_handled_yet";
public static final String DISCRIMINATOR_BAD_PATH = "DISCRIMINATOR_BAD_PATH";
public static final String DISCRIMINATOR__IS_BASED_ON_ELEMENT_EXISTENCE_BUT_SLICE__NEITHER_SETS_MIN1_OR_MAX0 = "Discriminator__is_based_on_element_existence_but_slice__neither_sets_min1_or_max0";
public static final String DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_MULTIPLE_TYPES = "Discriminator__is_based_on_type_but_slice__in__has_multiple_types";
public static final String DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_NO_TYPES = "Discriminator__is_based_on_type_but_slice__in__has_no_types";
public static final String DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF = "Display_Name_for__should_be_one_of__instead_of";
public static final String DISPLAY_NAME_WS_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF = "Display_Name_WS_for__should_be_one_of__instead_of";
@ -475,7 +474,6 @@ public class I18nConstants {
public static final String PROBLEM_PROCESSING_EXPRESSION__IN_PROFILE__PATH__ = "Problem_processing_expression__in_profile__path__";
public static final String PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE_WITH_A_PROFILE__IN_PROFILE_ = "Profile_based_discriminators_must_have_a_type_with_a_profile__in_profile_";
public static final String PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE__IN_PROFILE_ = "Profile_based_discriminators_must_have_a_type__in_profile_";
public static final String PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_ONLY_ONE_TYPE__IN_PROFILE = "Profile_based_discriminators_must_have_only_one_type__in_profile";
public static final String PROFILE_EXT_NOT_HERE = "Profile_EXT_Not_Here";
public static final String PROFILE_VAL_MISSINGELEMENT = "Profile_VAL_MissingElement";
public static final String PROFILE_VAL_NOTALLOWED = "Profile_VAL_NotAllowed";
@ -1123,4 +1121,9 @@ public class I18nConstants {
public static final String CODESYSTEM_CS_COMPLETE_AND_EMPTY = "CODESYSTEM_CS_COMPLETE_AND_EMPTY";
public static final String VALIDATION_VAL_VERSION_NOHASH = "VALIDATION_VAL_VERSION_NOHASH";
public static final String PRIMITIVE_TOO_SHORT = "PRIMITIVE_TOO_SHORT";
public static final String CANONICAL_MULTIPLE_VERSIONS_KNOWN = "CANONICAL_MULTIPLE_VERSIONS_KNOWN";
public static final String SD_PATH_NO_SLICING = "SD_PATH_NO_SLICING";
public static final String SD_PATH_SLICING_DEPRECATED = "SD_PATH_SLICING_DEPRECATED";
public static final String SD_PATH_NOT_VALID = "SD_PATH_NOT_VALID";
public static final String SD_PATH_ERROR = "SD_PATH_ERROR";
}

View File

@ -14,7 +14,6 @@ import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
@ -24,7 +23,7 @@ import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.http.HTTPAuthenticationMode;
import org.hl7.fhir.utilities.http.HTTPResult;
import org.hl7.fhir.utilities.http.ManagedWebAccess;
import org.hl7.fhir.utilities.http.ManagedWebAccessBuilder;
import org.hl7.fhir.utilities.http.ManagedWebAccessor;
import org.hl7.fhir.utilities.json.model.JsonArray;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.model.JsonProperty;
@ -176,13 +175,13 @@ public class PackageClient {
}
private InputStream fetchUrl(String source, String accept) throws IOException {
ManagedWebAccessBuilder client = ManagedWebAccess.builder().withAccept(accept);
ManagedWebAccessor webAccessor = ManagedWebAccess.accessor();
if (server.getAuthenticationMode() == HTTPAuthenticationMode.TOKEN) {
client.withToken(server.getToken());
webAccessor.withToken(server.getToken());
} else if (server.getAuthenticationMode() == HTTPAuthenticationMode.BASIC) {
client.withBasicAuth(server.getUsername(), server.getPassword());
webAccessor.withBasicAuth(server.getUsername(), server.getPassword());
}
HTTPResult res = client.get(source);
HTTPResult res = webAccessor.get(source, accept);
res.checkThrowException();
return new ByteArrayInputStream(res.getContent());
}

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -232,6 +233,14 @@ public class FhirSettings {
if (instance.fhirSettings.getPackageManagement() == null) {
return Collections.emptyList();
}
return List.of(instance.fhirSettings.getPackageManagement().getServers().toArray(new ServerDetailsPOJO[]{}));
return Arrays.asList(instance.fhirSettings.getPackageManagement().getServers().toArray(new ServerDetailsPOJO[]{}));
}
public static List<ServerDetailsPOJO> getTerminologyServers() {
getInstance();
if (instance.fhirSettings.getTerminologyServers() == null) {
return Collections.emptyList();
}
return Arrays.asList(instance.fhirSettings.getTerminologyServers().getServers().toArray(new ServerDetailsPOJO[]{}));
}
}

View File

@ -179,8 +179,6 @@ Did_not_find_type_root_ = Did not find type root: {0}
Differential_does_not_have_a_slice__b_of_____in_profile_ = Differential in profile {5} does not have a slice at {6} (on {0}, position {1} of {2} / {3} / {4})
Differential_walks_into____but_the_base_does_not_and_there_is_not_a_single_fixed_type_The_type_is__This_is_not_handled_yet = Differential walks into ''{0} (@ {1})'', but the base does not, and there is not a single fixed type. The type is {2}. This is not handled yet
Discriminator__is_based_on_element_existence_but_slice__neither_sets_min1_or_max0 = Discriminator ({0}) is based on element existence, but slice {1} neither sets min>=1 or max=0
Discriminator__is_based_on_type_but_slice__in__has_multiple_types_one =
Discriminator__is_based_on_type_but_slice__in__has_multiple_types_other = Discriminator ({1}) is based on type, but slice {2} in {3} has {0} types: {4}
Discriminator__is_based_on_type_but_slice__in__has_no_types = Discriminator ({0}) is based on type, but slice {1} in {2} has no types
Display_Name_WS_for__should_be_one_of__instead_of_one = Wrong whitespace in Display Name ''{4}'' for {1}#{2}. Valid display is {3} (for the language(s) ''{5}'')
Display_Name_WS_for__should_be_one_of__instead_of_other = Wrong whitespace in Display Name ''{4}'' for {1}#{2}. Valid display is one of {0} choices: {3} (for the language(s) ''{5}'')
@ -486,8 +484,6 @@ Profile___has_no_base_and_no_snapshot = Profile {0} ({1}) has no base and no sna
Profile__does_not_match_for__because_of_the_following_profile_issues__ = Profile {0} does not match for {1} because of the following profile issues: {2}
Profile_based_discriminators_must_have_a_type__in_profile_ = Profile based discriminators must have a type ({0} in profile {1})
Profile_based_discriminators_must_have_a_type_with_a_profile__in_profile_ = Profile based discriminators must have a type with a profile ({0} in profile {1})
Profile_based_discriminators_must_have_only_one_type__in_profile_one =
Profile_based_discriminators_must_have_only_one_type__in_profile_other = Profile based discriminators must have only one type ({1} in profile {2}) but found {0} types
QUESTIONNAIRE_QR_ITEM_BADOPTION_CS = The code provided {1} cannot be validated in the options value set ({2}) in the questionnaire because the system {0} could not be found
QUESTIONNAIRE_Q_DERIVATION_TYPE_IGNORED = The derivation type ''{0}'' means that no derivation checking has been performed against this questionnaire
QUESTIONNAIRE_Q_DERIVATION_TYPE_UNKNOWN = The derivation type ''{0}'' is unknown, which means that no derivation checking has been performed against this questionnaire
@ -1091,7 +1087,7 @@ XHTML_XHTML_NS_InValid = Wrong namespace on the XHTML (''{0}'', should be ''{1}'
XHTML_XHTML_Name_Invalid = Wrong name on the XHTML (''{0}'') - must start with div
XSI_TYPE_UNNECESSARY = xsi:type is unnecessary at this point
XSI_TYPE_WRONG = The xsi:type value ''{0}'' is wrong (should be ''{1}''). Note that xsi:type is unnecessary at this point
_DT_Fixed_Wrong = Value is ''{0}'' but must be ''{1}''{2}
_DT_Fixed_Wrong = Value is ''{0}'' but is fixed to ''{1}'' in the profile {2}
_has_children__and_multiple_types__in_profile_ = {0} has children ({1}) and multiple types ({2}) in profile {3}
_has_children__for_type__in_profile__but_cant_find_type = {0} has children ({1}) for type {2} in profile {3}, but can''t find type
_has_no_children__and_no_types_in_profile_ = {0} has no children ({1}) and no types in profile {2}
@ -1156,4 +1152,8 @@ SD_ED_ADDITIONAL_BINDING_USAGE_INVALID_TYPE = The Usage Context value must be of
CODESYSTEM_CS_COMPLETE_AND_EMPTY = When a CodeSystem has content = ''complete'', it doesn't make sense for there to be no concepts defined
VALIDATION_VAL_VERSION_NOHASH = Version ''{0}'' contains a ''#'', which as this character is used in some URLs to separate the version and the fragment id. When version does include '#', systems will not be able to parse the URL
PRIMITIVE_TOO_SHORT = Value ''{0}'' is shorter than permitted minimum length of {1}
CANONICAL_MULTIPLE_VERSIONS_KNOWN = The version {2} for the {0} {1} is not known. These versions are known: {3}
SD_PATH_SLICING_DEPRECATED = The discriminator type ''{0}'' has been deprecated. Use type=fixed with a pattern[x] instead
SD_PATH_NOT_VALID = The discriminator path ''{0}'' does not appear to be valid for the element that is being sliced ''{1}''
SD_PATH_ERROR = The discriminator path ''{0}'' does not appear to be valid for the element that is being sliced ''{1}'': {2}

View File

@ -0,0 +1,20 @@
package org.hl7.fhir.utilities.http;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class HTTPHeaderTests {
@Test
void testHTTPHeaderEquals() {
HTTPHeader header1 = new HTTPHeader("name", "value1");
HTTPHeader sameAsHeader1 = new HTTPHeader("name", "value1");
assertThat(header1).isEqualTo(sameAsHeader1);
HTTPHeader notSameNameAsHeader1 = new HTTPHeader("name2", "value1");
assertThat(header1).isNotEqualTo(notSameNameAsHeader1);
HTTPHeader notSameValueAsHeader1 = new HTTPHeader("name", "value2");
assertThat(header1).isNotEqualTo(notSameValueAsHeader1);
}
}

View File

@ -0,0 +1,40 @@
package org.hl7.fhir.utilities.http;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class ManagedFhirWebAccessorTests {
final String expectedUserAgent = "dummy-agent";
@Test
@DisplayName("Test default headers are added correctly.")
void addDefaultAgentHeader() {
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com");
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(expectedUserAgent, null);
HTTPRequest requestWithDefaultHeaders = builder.httpRequestWithDefaultHeaders(request);
assertRequestContainsExpectedAgentHeader(requestWithDefaultHeaders);
}
@Test
@DisplayName("Test default headers are added correctly.")
void addDefaultBasicHeader() {
HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com");
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(expectedUserAgent, null)
.withBasicAuth("dummy-user", "dummy-password");
HTTPRequest requestWithManagedHeaders = builder.requestWithManagedHeaders(request);
assertRequestContainsExpectedAgentHeader(requestWithManagedHeaders);
Assertions.assertNotNull(HTTPHeaderUtil.getSingleHeader(requestWithManagedHeaders.getHeaders(),"Authorization"), "Authorization header null.");
}
private void assertRequestContainsExpectedAgentHeader(HTTPRequest requestWithDefaultHeaders) {
Assertions.assertNotNull(HTTPHeaderUtil.getSingleHeader(requestWithDefaultHeaders.getHeaders(),"User-Agent"), "User-Agent header null.");
Assertions.assertEquals(expectedUserAgent, HTTPHeaderUtil.getSingleHeader(requestWithDefaultHeaders.getHeaders(),"User-Agent"),
"User-Agent header not populated with expected value \""+expectedUserAgent+"\".");
}
}

View File

@ -0,0 +1,216 @@
package org.hl7.fhir.utilities.http;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.apache.commons.net.util.Base64;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class ManagedWebAccessAuthTests {
public static final String DUMMY_AGENT = "dummyAgent";
public static final String DUMMY_USERNAME = "dummy1";
public static final String DUMMY_PASSWORD = "pass1";
public static final String DUMMY_TOKEN = "dummyToken";
private static final String DUMMY_API_KEY = "dummyApiKey";
private MockWebServer server;
@BeforeEach
void setup() {
setupMockServer();
}
void setupMockServer() {
server = new MockWebServer();
}
@Test
public void testBaseCase() throws IOException, InterruptedException {
HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah");
server.enqueue(
new MockResponse()
.setBody("Dummy Response").setResponseCode(200)
);
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null);
HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET));
assertThat(result.getCode()).isEqualTo(200);
assertThat(result.getContentAsString()).isEqualTo("Dummy Response");
RecordedRequest packageRequest = server.takeRequest();
assert packageRequest.getRequestUrl() != null;
assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET");
}
@Test
public void testBasicAuthCase() throws IOException, InterruptedException {
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null).withBasicAuth("dummy1", "pass1");
testBasicServerAuth(builder);
}
private void testBasicServerAuth(ManagedFhirWebAccessor builder) throws IOException, InterruptedException {
HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah");
server.enqueue(
new MockResponse()
.setBody("Dummy Response").setResponseCode(200)
);
HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET));
assertThat(result.getCode()).isEqualTo(200);
assertThat(result.getContentAsString()).isEqualTo("Dummy Response");
RecordedRequest packageRequest = server.takeRequest();
assert packageRequest.getRequestUrl() != null;
assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET");
byte[] b = Base64.encodeBase64((DUMMY_USERNAME + ":" + DUMMY_PASSWORD).getBytes(StandardCharsets.US_ASCII));
String b64 = new String(b, StandardCharsets.US_ASCII);
assertThat(packageRequest.getHeader("Authorization")).isEqualTo("Basic " + b64);
}
@Test
public void testTokenAuthCase() throws IOException, InterruptedException {
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null).withToken(DUMMY_TOKEN);
testTokenAuthCase(builder);
}
private void testTokenAuthCase(ManagedFhirWebAccessor builder) throws IOException, InterruptedException {
HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah");
server.enqueue(
new MockResponse()
.setBody("Dummy Response").setResponseCode(200)
);
HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET));
assertThat(result.getCode()).isEqualTo(200);
assertThat(result.getContentAsString()).isEqualTo("Dummy Response");
RecordedRequest packageRequest = server.takeRequest();
assert packageRequest.getRequestUrl() != null;
assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET");
assertThat(packageRequest.getHeader("Authorization")).isEqualTo("Bearer " + DUMMY_TOKEN);
}
private void assertExpectedHeaders(RecordedRequest packageRequest, String expectedUrl, String expectedHttpMethod) {
assertThat(packageRequest.getRequestUrl().toString()).isEqualTo(expectedUrl);
assertThat(packageRequest.getMethod()).isEqualTo(expectedHttpMethod);
assertThat(packageRequest.getHeader("User-Agent")).isEqualTo(DUMMY_AGENT);
}
@Test
public void testApiKeyAuthCase() throws IOException, InterruptedException {
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null).withApiKey(DUMMY_API_KEY);
testApiKeyAuthCase(builder);
}
private void testApiKeyAuthCase(ManagedFhirWebAccessor builder) throws IOException, InterruptedException {
HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah");
server.enqueue(
new MockResponse()
.setBody("Dummy Response").setResponseCode(200)
);
HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET));
assertThat(result.getCode()).isEqualTo(200);
assertThat(result.getContentAsString()).isEqualTo("Dummy Response");
RecordedRequest packageRequest = server.takeRequest();
assert packageRequest.getRequestUrl() != null;
assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET");
assertThat(packageRequest.getHeader("Api-Key")).isEqualTo(DUMMY_API_KEY);
}
@Test
public void testBasicAuthFromSettings() throws IOException, InterruptedException {
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(
"dummyAgent",
List.of(getBasicAuthServerPojo()));
testBasicServerAuth(builder);
}
private ServerDetailsPOJO getBasicAuthServerPojo() {
return new ServerDetailsPOJO(
server.url("").toString(),
"basic",
"dummyServerType",
DUMMY_USERNAME,
DUMMY_PASSWORD,
null, null);
}
@Test
public void testTokenAuthFromSettings() throws IOException, InterruptedException {
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(
"dummyAgent",
List.of(getTokenAuthServerPojo()));
testTokenAuthCase(builder);
}
private ServerDetailsPOJO getTokenAuthServerPojo() {
return new ServerDetailsPOJO(
server.url("").toString(),
"token",
"dummyServerType",
null,
null,
DUMMY_TOKEN, null);
}
@Test
public void testApiKeyAuthFromSettings() throws IOException, InterruptedException {
ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(
"dummyAgent",
List.of(getApiKeyAuthServerPojo()));
testApiKeyAuthCase(builder);
}
private ServerDetailsPOJO getApiKeyAuthServerPojo() {
return new ServerDetailsPOJO(
server.url("").toString(),
"apikey",
"dummyServerType",
null,
null,
null, DUMMY_API_KEY);
}
@Test
public void verifyAllowedPaths() {
//TODO the allowed paths cannot be set for now, meaning all will be allowed.
ManagedWebAccess.inAllowedPaths("http://www.anywhere.com");
}
}

View File

@ -71,6 +71,7 @@ import org.hl7.fhir.utilities.SystemExitManager;
import org.hl7.fhir.utilities.TimeTracker;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.http.ManagedWebAccess;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.validation.cli.model.CliContext;
import org.hl7.fhir.validation.cli.services.ValidationService;
@ -151,6 +152,7 @@ public class ValidatorCli {
if (cliContext.getFhirSettingsFile() != null) {
FhirSettings.setExplicitFilePath(cliContext.getFhirSettingsFile());
}
ManagedWebAccess.loadFromFHIRSettings();
FileFormat.checkCharsetAndWarnIfNotUTF8(System.out);

View File

@ -1047,7 +1047,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
private boolean check(String v1, String v2) {
return v1 == null ? Utilities.noString(v1) : v1.equals(v2);
boolean res = v1 == null ? Utilities.noString(v1) : v1.equals(v2);
return res;
}
private boolean checkAddress(List<ValidationMessage> errors, String path, Element focus, Address fixed, String fixedSource, boolean pattern, String context) {
@ -3209,10 +3210,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
if (context.hasFixed()) {
ok = checkFixedValue(errors, path, e, context.getFixed(), profile.getVersionedUrl(), context.getSliceName(), null, false, "") && ok;
ok = checkFixedValue(errors, path, e, context.getFixed(), profile.getVersionedUrl(), context.getSliceName(), null, false, profile.getVersionedUrl()+"#"+context.getId()) && ok;
}
if (context.hasPattern()) {
ok = checkFixedValue(errors, path, e, context.getPattern(), profile.getVersionedUrl(), context.getSliceName(), null, true, "") && ok;
ok = checkFixedValue(errors, path, e, context.getPattern(), profile.getVersionedUrl(), context.getSliceName(), null, true, profile.getVersionedUrl()+"#"+context.getId()) && ok;
}
if (ok && !ID_EXEMPT_LIST.contains(e.fhirType())) { // ids get checked elsewhere
@ -5129,12 +5130,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if ("0".equals(criteriaElement.getMax())) {
expression.append(" and " + discriminator + ".empty()");
} else if (s.getType() == DiscriminatorType.TYPE) {
String type = null;
if (!criteriaElement.getPath().contains("[") && discriminator.contains("[")) {
discriminator = discriminator.substring(0, discriminator.indexOf('['));
String lastNode = tail(discriminator);
type = makeTypeForFHIRPath(criteriaElement.getPath()).substring(lastNode.length());
String type = makeTypeForFHIRPath(criteriaElement.getPath()).substring(lastNode.length());
expression.append(" and " + discriminator + " is " + type);
} else if (!criteriaElement.hasType() || criteriaElement.getType().size() == 1) {
String type = null;
if (discriminator.contains("["))
discriminator = discriminator.substring(0, discriminator.indexOf('['));
if (criteriaElement.hasType()) {
@ -5144,23 +5146,25 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} else {
throw new DefinitionException(context.formatMessage(I18nConstants.DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_NO_TYPES, discriminator, ed.getId(), profile.getVersionedUrl()));
}
expression.append(" and " + discriminator + " is " + type);
} else if (criteriaElement.getType().size() > 1) {
throw new DefinitionException(context.formatMessagePlural(criteriaElement.getType().size(), I18nConstants.DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_MULTIPLE_TYPES, discriminator, ed.getId(), profile.getVersionedUrl(), criteriaElement.typeSummary()));
CommaSeparatedStringBuilder cb = new CommaSeparatedStringBuilder(" or ");
for (TypeRefComponent tr : criteriaElement.getType()) {
String type = makeTypeForFHIRPath(tr.getWorkingCode());
cb.append(discriminator + " is " + type);
}
expression.append(" and (" + cb.toString()+")");
} else
throw new DefinitionException(context.formatMessage(I18nConstants.DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_NO_TYPES, discriminator, ed.getId(), profile.getVersionedUrl()));
if (discriminator.isEmpty()) {
expression.append(" and $this is " + type);
} else {
expression.append(" and " + discriminator + " is " + type);
}
} else if (s.getType() == DiscriminatorType.PROFILE) {
if (criteriaElement.getType().size() == 0) {
throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE__IN_PROFILE_, criteriaElement.getId(), profile.getVersionedUrl()));
}
if (criteriaElement.getType().size() != 1) {
throw new DefinitionException(context.formatMessagePlural(criteriaElement.getType().size(), I18nConstants.PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_ONLY_ONE_TYPE__IN_PROFILE, criteriaElement.getId(), profile.getVersionedUrl()));
List<CanonicalType> list = new ArrayList<>();
boolean ref = discriminator.endsWith(".resolve()") || discriminator.equals("resolve()");
for (TypeRefComponent tr : criteriaElement.getType()) {
list.addAll(ref ? tr.getTargetProfile() : tr.getProfile());
}
List<CanonicalType> list = discriminator.endsWith(".resolve()") || discriminator.equals("resolve()") ? criteriaElement.getType().get(0).getTargetProfile() : criteriaElement.getType().get(0).getProfile();
if (list.size() == 0) {
// we don't have to find something
// throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE_WITH_A_PROFILE__IN_PROFILE_, criteriaElement.getId(), profile.getVersionedUrl()));
@ -6357,10 +6361,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
ValidationInfo vi = element.addDefinition(profile, definition, mode);
if (definition.getFixed() != null) {
ok = checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getVersionedUrl(), definition.getSliceName(), null, false, "") && ok;
ok = checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), profile.getVersionedUrl(), definition.getSliceName(), null, false, profile.getVersionedUrl()+"#"+definition.getId()) && ok;
}
if (definition.getPattern() != null) {
ok = checkFixedValue(errors, stack.getLiteralPath(), element, definition.getPattern(), profile.getVersionedUrl(), definition.getSliceName(), null, true, "") && ok;
ok = checkFixedValue(errors, stack.getLiteralPath(), element, definition.getPattern(), profile.getVersionedUrl(), definition.getSliceName(), null, true, profile.getVersionedUrl()+"#"+definition.getId()) && ok;
}
// get the list of direct defined children, including slices
@ -6644,10 +6648,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
ok = checkPrimitive(valContext, errors, ei.getPath(), type, checkDefn, ei.getElement(), profile, localStack, stack, valContext.getRootResource()) && ok;
} else {
if (checkDefn.hasFixed()) {
ok = checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getFixed(), profile.getVersionedUrl(), checkDefn.getSliceName(), null, false, "") && ok;
ok = checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getFixed(), profile.getVersionedUrl(), checkDefn.getSliceName(), null, false, profile.getVersionedUrl()+"#"+definition.getId()) && ok;
}
if (checkDefn.hasPattern()) {
ok = checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getPattern(), profile.getVersionedUrl(), checkDefn.getSliceName(), null, true, "") && ok;
ok = checkFixedValue(errors, ei.getPath(), ei.getElement(), checkDefn.getPattern(), profile.getVersionedUrl(), checkDefn.getSliceName(), null, true, profile.getVersionedUrl()+"#"+definition.getId()) && ok;
}
}
if (type.equals("Identifier")) {
@ -7600,7 +7604,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private boolean valueMatchesCriteria(Element value, ElementDefinition criteria, StructureDefinition profile) throws FHIRException {
if (criteria.hasFixed()) {
List<ValidationMessage> msgs = new ArrayList<ValidationMessage>();
checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getVersionedUrl(), "value", null, false, "");
checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getVersionedUrl(), "value", null, false, profile.getVersionedUrl()+"#"+criteria.getId());
return msgs.size() == 0;
} else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) {
throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SLICE_MATCHING__SLICE_MATCHING_BY_VALUE_SET_NOT_DONE));

View File

@ -237,6 +237,17 @@ public class MeasureValidator extends BaseValidator {
NodeStack ns = stack.push(m, -1, m.getProperty().getDefinition(), m.getProperty().getDefinition());
hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, m.line(), m.col(), ns.getLiteralPath(), Utilities.existsInList(mc.scoring(), "proportion", "ratio", "continuous-variable", "cohort"), I18nConstants.MEASURE_MR_M_SCORING_UNK);
ok = validateMeasureReportGroups(hostContext, mc, errors, element, stack, inComplete) && ok;
} else {
if (measure.contains("|")) {
List<Measure> versionList = context.fetchResourcesByUrl(Measure.class, measure.substring(0, measure.indexOf("|")));
if (versionList != null && !versionList.isEmpty()) {
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (Measure mm : versionList) {
b.append(mm.getVersion());
}
hint(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, m.line(), m.col(), stack.getLiteralPath(), msrc != null, I18nConstants.CANONICAL_MULTIPLE_VERSIONS_KNOWN, "Measure", measure, measure.substring(measure.indexOf("|")+1), b.toString());
}
}
}
}
return ok;

View File

@ -16,6 +16,7 @@ import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.Manager;
@ -23,6 +24,7 @@ import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r5.extensions.ExtensionConstants;
import org.hl7.fhir.r5.fhirpath.ExpressionNode;
import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus;
import org.hl7.fhir.r5.fhirpath.FHIRLexer.FHIRLexerException;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IssueMessage;
import org.hl7.fhir.r5.fhirpath.TypeDetails;
@ -193,7 +195,7 @@ public class StructureDefinitionValidator extends BaseValidator {
}
}
} catch (Exception e) {
//e.printStackTrace();
e.printStackTrace();
rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage());
ok = false;
}
@ -445,6 +447,44 @@ public class StructureDefinitionValidator extends BaseValidator {
addCharacteristics(characteristics, path);
characteristicsValid = true;
}
if (element.hasChild("slicing")) {
Element slicing = element.getNamedChild("slicing");
NodeStack sStack = stack.push(slicing, -1, null, null);
// validating slicing.
// slicing can only be present if base cardinality > 1, but we can't always know that on the differential - though we can look it up
String tn = path.contains(".") ? path.substring(0, path.indexOf(".")) : path;
StructureDefinition tsd = context.fetchTypeDefinition(tn);
ElementDefinition ted = null;
if (tsd != null) {
ted = tsd.getSnapshot().getElementByPath(path);
if (ted != null) {
ok = rule(errors, "2022-11-02", IssueType.NOTFOUND, sStack, canSlice(ted), I18nConstants.SD_PATH_NO_SLICING, path) && ok;
}
}
int i = 0;
for (Element discriminator : slicing.getChildren("discriminator")) {
NodeStack dStack = sStack.push(discriminator, i, null, null);
String type = discriminator.getNamedChildValue("type");
warning(errors, "2024-11-06", IssueType.BUSINESSRULE, dStack, !"pattern".equals(type), I18nConstants.SD_PATH_SLICING_DEPRECATED, type);
String pathExp = discriminator.getNamedChildValue("path");
if (ted != null) {
TypeDetails td = getTypesForElement(elements, element, tn, tsd.getUrl());
if (!td.isEmpty()) {
List<IssueMessage> warnings = new ArrayList<IssueMessage>();
try {
TypeDetails eval = fpe.checkOnTypes(this, tn, td, fpe.parse(pathExp), warnings, true);
if (eval.isEmpty()) {
ok = rule(errors, "2024-11-06", IssueType.INVALID, dStack, false, I18nConstants.SD_PATH_NOT_VALID, pathExp, path) && ok;
}
} catch (Exception e) {
ok = rule(errors, "2024-11-06", IssueType.INVALID, dStack, false, I18nConstants.SD_PATH_ERROR, pathExp, path, e.getMessage()) && ok;
}
}
}
}
}
if (!snapshot && (element.hasChild("fixed") || element.hasChild("pattern")) && base != null) {
ElementDefinition ed = getDefinitionFromBase(base, element.getNamedChildValue("id"), element.getNamedChildValue("path"));
@ -599,6 +639,10 @@ public class StructureDefinitionValidator extends BaseValidator {
return ok;
}
private boolean canSlice(ElementDefinition ted) {
return !("1".equals(ted.getMax())) || ted.getPath().contains("[x]");
}
private boolean checkTypeParameters(List<ValidationMessage> errors, NodeStack stack, Element typeE, String tc, StructureDefinition tsd, String path, StructureDefinition sd) {
boolean ok = true;
if (tsd.hasExtension(ToolingExtensions.EXT_TYPE_PARAMETER)) {

View File

@ -4,7 +4,6 @@ import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
@ -26,7 +25,6 @@ import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
import org.hl7.fhir.convertors.txClient.TerminologyClientFactory;
import org.hl7.fhir.exceptions.DefinitionException;
@ -41,19 +39,14 @@ import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.client.ITerminologyClient;
import org.hl7.fhir.r5.test.utils.CompareUtilities;
import org.hl7.fhir.r5.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtil;
import org.hl7.fhir.utilities.*;
import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
import org.hl7.fhir.utilities.http.HTTPHeader;
import org.hl7.fhir.utilities.json.JsonException;
import org.hl7.fhir.utilities.json.model.JsonArray;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.parser.JsonParser;
import okhttp3.internal.http2.Header;
public class TxTester {
@ -134,7 +127,7 @@ public class TxTester {
return true;
} else {
System.out.println(software+" did not pass all HL7 terminology service tests (modes "+m+", tests v"+loadVersion()+", runner v"+VersionUtil.getBaseVersion()+")");
System.out.println("Failed Tests: "+CommaSeparatedStringBuilder.join(",", fails ));
System.out.println("Failed Tests: "+ CommaSeparatedStringBuilder.join(",", fails ));
return false;
}
} else {
@ -250,12 +243,12 @@ public class TxTester {
outputT.add("name", test.asString("name"));
if (Utilities.noString(filter) || filter.equals("*") || test.asString("name").contains(filter)) {
System.out.print(" Test "+test.asString("name")+": ");
Header header = null;
HTTPHeader header = null;
try {
if (test.has("header")) {
JsonObject hdr = test.getJsonObject("header");
if (hdr.has("mode") && modes.contains(hdr.asString("mode"))) {
header = new Header(hdr.asString("name"), hdr.asString("value"));
header = new HTTPHeader(hdr.asString("name"), hdr.asString("value"));
tx.getClientHeaders().addHeader(header);
}
}

View File

@ -0,0 +1,9 @@
import org.hl7.fhir.utilities.http.ManagedWebAccess;
import org.junit.BeforeClass;
public class ManagedWebAccessAuthTests {
@BeforeClass
public static void setUp() {
ManagedWebAccess.setUserAgent("hapi-fhir-testing-client");
}
}

View File

@ -434,7 +434,7 @@
while if true it will use an executable. -->
<fork>true</fork>
<meminitial>512m</meminitial>
<maxmem>768m</maxmem>
<maxmem>1000m</maxmem>
<debug>true</debug>
<compilerArgs>
<arg>-J-XX:MaxRAMPercentage=50.0</arg>

View File

@ -28,7 +28,7 @@ jobs:
options: '--settings $(Agent.TempDirectory)/settings.xml -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER) -DskipTests -DdeployToSonatype -P CHECKSTYLE'
${{ else }}:
options: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER) -DskipTests -P CHECKSTYLE'
mavenOptions: '-Xmx768m'
mavenOptions: '-Xmx1G'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.11'
jdkArchitectureOption: 'x64'