diff --git a/org.hl7.fhir.dstu3/pom.xml b/org.hl7.fhir.dstu3/pom.xml
index 8f61071b8..aef8d1dcc 100644
--- a/org.hl7.fhir.dstu3/pom.xml
+++ b/org.hl7.fhir.dstu3/pom.xml
@@ -28,6 +28,12 @@
hapi-fhir-base
+
+ org.projectlombok
+ lombok
+ provided
+
+
org.fhir
@@ -99,6 +105,13 @@
+
+ com.squareup.okhttp3
+ mockwebserver
+ true
+ test
+
+
com.fasterxml.jackson.core
jackson-databind
diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/FHIRToolingClient.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/FHIRToolingClient.java
index a596ffef2..90c319d71 100644
--- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/FHIRToolingClient.java
+++ b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/FHIRToolingClient.java
@@ -5,12 +5,12 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
+import lombok.Getter;
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;
@@ -27,8 +27,6 @@ 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;
/**
@@ -74,8 +72,9 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
private String password;
private String userAgent;
private EnumSet allowedVersions;
- private String acceptLang;
- private String contentLang;
+ @Getter
+ private String acceptLanguage;
+ private String contentLanguage;
private int useCount;
//Pass endpoint for client - URI
@@ -147,7 +146,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) {
@@ -161,7 +160,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) {
@@ -175,7 +174,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) {
@@ -190,7 +189,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()) {
@@ -208,7 +207,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()) {
@@ -226,7 +225,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()) {
@@ -250,7 +249,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()) {
@@ -278,7 +277,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()) {
@@ -317,13 +316,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());
@@ -360,7 +359,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());
@@ -417,7 +416,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) {
@@ -436,7 +435,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) {
@@ -455,7 +454,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) {
@@ -476,7 +475,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()) {
@@ -501,7 +500,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
- generateHeaders(),
+ generateHeaders(true),
"Closure?name=" + name,
timeoutNormal);
if (result.isUnsuccessfulRequest()) {
@@ -523,7 +522,7 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
withVer(getPreferredResourceFormat(), "3.0"),
- generateHeaders(),
+ generateHeaders(true),
"UpdateClosure?name=" + name,
timeoutOperation);
if (result.isUnsuccessfulRequest()) {
@@ -580,36 +579,38 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
headers.forEach(this.headers::add);
}
- private Headers generateHeaders() {
- Headers.Builder builder = new Headers.Builder();
+ //FIXME should be in ManagedWebAccess?
+ private Iterable generateHeaders(boolean hasBody) {
+ List headers = new ArrayList<>();
// Add basic auth header if it exists
if (basicAuthHeaderExists()) {
- builder.add(getAuthorizationHeader().toString());
+ headers.add(getAuthorizationHeader());
}
// Add any other headers
- if(this.headers != null) {
- this.headers.forEach(header -> builder.add(header.toString()));
- }
+ headers.addAll(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();
+
+ if (hasBody && !Utilities.noString(contentLanguage)) {
+ headers.add(new HTTPHeader("Content-Language",contentLanguage));
+ }
+
+ return headers;
}
public boolean basicAuthHeaderExists() {
return (username != null) && (password != null);
}
- public Header getAuthorizationHeader() {
+ public HTTPHeader getAuthorizationHeader() {
String usernamePassword = username + ":" + password;
String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
- return new Header("Authorization", "Basic " + base64usernamePassword);
+ return new HTTPHeader("Authorization", "Basic " + base64usernamePassword);
}
public String getUserAgent() {
@@ -626,10 +627,10 @@ public class FHIRToolingClient extends FHIRBaseToolingClient {
}
public void setAcceptLanguage(String lang) {
- this.acceptLang = lang;
+ this.acceptLanguage = lang;
}
public void setContentLanguage(String lang) {
- this.contentLang = lang;
+ this.contentLanguage = lang;
}
public int getUseCount() {
diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/Client.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/Client.java
index a0635aadf..6ce3da987 100644
--- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/Client.java
+++ b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/Client.java
@@ -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,30 @@ public class Client {
String resourceFormat,
String message,
long timeout) throws IOException {
+ /*FIXME delete after refactor
Request.Builder request = new Request.Builder()
.method("OPTIONS", null)
.url(optionsUri.toURL());
+ */
+ HTTPRequest request = new HTTPRequest()
+ .withUrl(optionsUri.toURL())
+ .withMethod(HTTPRequest.HttpMethod.OPTIONS);
- return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
+ return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout);
}
public ResourceRequest issueGetResourceRequest(URI resourceUri,
String resourceFormat,
- Headers headers,
+ Iterable headers,
String message,
long timeout) throws IOException {
+ /*FIXME delete after refactor
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 +95,26 @@ 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 ResourceRequest issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
- Headers headers,
+ Iterable headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("PUT requests require a non-null payload");
+ /*FIXME delete after refactor
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 +123,50 @@ 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 ResourceRequest issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
- Headers headers,
+ Iterable headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
+ /*FIXME delete after refactor
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 {
+ /*FIXME delete after refactor
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 {
+ /*FIXME delete after refactor
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 +176,18 @@ public class Client {
String resourceFormat) throws IOException {
String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy";
byte[] payload = ByteUtils.encodeFormSubmission(parameters, resourceName, resource, boundary);
+ /*FIXME delete after refactor
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 executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout);
+ return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
@@ -163,17 +196,22 @@ public class Client {
String message,
int timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
+ /*FIXME delete after refactor
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 Bundle executeBundleRequest(Request.Builder request,
+ public Bundle executeBundleRequest(HTTPRequest request,
String resourceFormat,
- Headers headers,
+ Iterable headers,
String message,
int retryCount,
long timeout) throws IOException {
@@ -182,23 +220,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 ResourceRequest executeFhirRequest(Request.Builder request,
- String resourceFormat,
- Headers headers,
- String message,
- int retryCount,
- long timeout) throws IOException {
+ public ResourceRequest executeFhirRequest(HTTPRequest request,
+ String resourceFormat,
+ Iterable 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();
}
diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilder.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilder.java
index ad15ae999..86f5b9806 100644
--- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilder.java
+++ b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilder.java
@@ -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 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
+ * Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s 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}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 headers) {
+ List allHeaders = new ArrayList<>();
+ request.getHeaders().forEach(allHeaders::add);
- /**
- * Adds necessary headers for all REST requests.
- * User-Agent : hapi-fhir-tooling-client
- *
- * @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 getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
+ List 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}. 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> 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 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.
- *
- * 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 ManagedFhirWebAccessBuilder getManagedWebAccessBuilder() {
+ return new ManagedFhirWebAccessBuilder("hapi-fhir-tooling-client", null).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 headers) {
this.headers = headers;
return this;
}
@@ -227,25 +149,16 @@ public class FhirRequestBuilder {
return this;
}
- protected Request buildRequest() {
- return httpRequest.build();
- }
-
public ResourceRequest 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 = getManagedWebAccessBuilder().httpCall(requestWithHeaders);
T resource = unmarshalReference(response, resourceFormat);
- return new ResourceRequest(resource, response.code(), getLocationHeader(response.headers()));
+ return new ResourceRequest(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 = getManagedWebAccessBuilder().httpCall(requestWithHeaders);
return unmarshalFeed(response, resourceFormat);
}
@@ -253,14 +166,14 @@ public class FhirRequestBuilder {
* Unmarshalls a resource from the response stream.
*/
@SuppressWarnings("unchecked")
- protected T unmarshalReference(Response response, String format) {
+ protected 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);
@@ -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 headerList = new ArrayList<>(Collections.emptyList());
- Map> 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 headerList = new ArrayList<>(Collections.emptyList());
- Map> 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?");
-// }
- }
}
diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/RetryInterceptor.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/RetryInterceptor.java
deleted file mode 100644
index e3b6ec084..000000000
--- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/RetryInterceptor.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.hl7.fhir.dstu3.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);
- }
-
-}
\ No newline at end of file
diff --git a/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/FhirToolingClientTest.java b/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/FhirToolingClientTest.java
new file mode 100644
index 000000000..54f363798
--- /dev/null
+++ b/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/FhirToolingClientTest.java
@@ -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> headersArgumentCaptor;
+
+
+ @BeforeEach
+ void setUp() throws IOException, URISyntaxException {
+ MockitoAnnotations.openMocks(this);
+ mockClient = Mockito.mock(Client.class);
+ ResourceRequest 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 getHeaders() {
+ return new ArrayList<>(Arrays.asList(h1, h2, h3));
+ }
+
+ private List 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 argumentCaptorValue) {
+ List 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 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 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 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 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 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 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 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 argumentCaptorValue = headersArgumentCaptor.getValue();
+ checkHeaders(argumentCaptorValue);
+ }
+}
diff --git a/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/ClientTest.java b/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/ClientTest.java
new file mode 100644
index 000000000..18f1bed48
--- /dev/null
+++ b/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/ClientTest.java
@@ -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 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 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 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 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 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());
+ }
+}
diff --git a/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilderTests.java b/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilderTests.java
index 19af3ea75..d2f85b520 100644
--- a/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilderTests.java
+++ b/org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/FhirRequestBuilderTests.java
@@ -1,111 +1,128 @@
package org.hl7.fhir.dstu3.utils.client.network;
-import java.io.IOException;
-import java.net.MalformedURLException;
+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() {
+ //FIXME tested here. Should get list of HTTPHeader.
+ 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 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;
-
- public FhirRequestBuilderTests() throws MalformedURLException {
- }
-
- @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> 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() {
+ //FIXME tested here. Should get list of HTTPHeader.
+ 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 headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat);
+
+ Map> 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 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 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()));
+ }
+
+
}