Make dstu3 use ManagedFhirWebAccess

This commit is contained in:
dotasek 2024-10-30 13:11:51 -04:00
parent 2f95e3fe9f
commit eb7144053d
8 changed files with 672 additions and 406 deletions

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

@ -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<FhirPublication> 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<String, String>()),
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<String, String>()),
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<HTTPHeader> generateHeaders(boolean hasBody) {
List<HTTPHeader> 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() {

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,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 <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
Headers headers,
Iterable<HTTPHeader> 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 <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");
/*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 <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");
/*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 <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 +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 <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

@ -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
* 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<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 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<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 = getManagedWebAccessBuilder().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 = 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 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);
@ -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

@ -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);
}
}

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,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<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;
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<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() {
//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<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()));
}
}