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())); + } + + }