From 59fe0c6d20a6f3e87ac66726430252178e5e9d53 Mon Sep 17 00:00:00 2001 From: dotasek Date: Fri, 8 Nov 2024 16:42:26 -0500 Subject: [PATCH] Manage FhirRequestBuilders via ManagedWebAccess (#1788) * WIP start moving FhirRequestBuilder to ManagedWebAccess * Update PECodeGenerator.java * WIP keep moving FhirRequestBuilder to ManagedWebAccess + fix some tests * WIP move logging, retry, timeout and proxy * WIP move accept to parameter * WIP some HTTP Header refactoring, rename FhirRequest to HTTPRequest * WIP fix removed import * Tidy up code ***NO_CI*** * Fix for renamed class * Change mem settings for build * Adjust pipeline mem again * Apply changes to r4b * Actually add headers from HTTPRequest to OkHttp request * Use ManagedFhirWebAccess in r4 * Removing leftover okttp usage * Remove unused classes and okhttp usages and add tests for r4 * Make dstu3 use ManagedFhirWebAccess * Make sure we get response headers * Add test coverage (some failing) for DSTU2 ClientUtils * Add FHIRToolingClient tests * Use ManagedFhirWebAccess for DSTU2 * WIP restore format header tests - turns out they break * Fix format header issues * Switch to static fhirBuilder for all FhirRequestBuilders + Fix null user agent issue + Load ManagedWebAccess from FHIR settings + Add terminology servers to FhirSettings * fix slicing by type and profile to allow multiple options per slice * List measure choices when a match by version can't be found * Validate fhirpath expression in slice discriminators * Remove conflicting authorization header setting methods * fix VSAC importer for changes to ManagedWebAccess * Tests 1 * Rename ...AccessBuilder classes to ...Accessor * Fix api-key token mixup, more tests * Resolve all deletions enclosed in FIXME + fix headers in dstu2 postfeed * More TODO erasures. Plus found missing functionality * Move tests to utilities. Clarify missing functionality * Clean up comments and JavaDoc * Final rename * update vsac access code * Fix get bytes for -1 or chunked content --------- Co-authored-by: Jens Kristian Villadsen Co-authored-by: Grahame Grieve Co-authored-by: Grahame Grieve --- RELEASE_NOTES.md | 3 + .../fhir/convertors/misc/ICD11Generator.java | 3 +- .../fhir/convertors/misc/VSACImporter.java | 9 +- org.hl7.fhir.dstu2/pom.xml | 24 + .../fhir/dstu2/utils/client/ClientUtils.java | 485 ++++++------------ .../dstu2/utils/client/FHIRToolingClient.java | 354 ++----------- .../dstu2/utils/client/ResourceAddress.java | 5 +- .../dstu2/utils/client/ResourceRequest.java | 15 +- .../dstu2/utils/client/ClientUtilsTest.java | 350 +++++++++++++ .../utils/client/FHIRToolingClientTest.java | 166 ++++++ org.hl7.fhir.dstu3/pom.xml | 13 + .../dstu3/utils/client/FHIRToolingClient.java | 151 ++---- .../dstu3/utils/client/network/Client.java | 105 ++-- .../utils/client/network/ClientHeaders.java | 37 +- .../client/network/FhirRequestBuilder.java | 247 ++------- .../utils/client/FhirToolingClientTest.java | 225 ++++++++ .../utils/client/network/ClientTest.java | 169 ++++++ .../network/FhirRequestBuilderTests.java | 167 +++--- org.hl7.fhir.r4/pom.xml | 13 +- .../r4/utils/client/FHIRToolingClient.java | 165 ++---- .../fhir/r4/utils/client/network/Client.java | 137 ++--- .../utils/client/network/ClientHeaders.java | 32 +- .../network/FhirLoggingInterceptor.java | 74 --- .../client/network/FhirRequestBuilder.java | 217 +++----- .../client/network/RetryInterceptor.java | 67 --- .../utils/client/FhirToolingClientTest.java | 224 ++++++++ .../client/network/ClientHeadersTest.java | 104 ++++ .../r4/utils/client/network/ClientTest.java | 140 +++++ .../network/FhirRequestBuilderTest.java | 128 +++++ org.hl7.fhir.r4b/pom.xml | 6 + .../TerminologyCacheManager.java | 12 +- .../r4b/utils/client/FHIRToolingClient.java | 136 ++--- .../fhir/r4b/utils/client/network/Client.java | 151 +++--- .../utils/client/network/ClientHeaders.java | 35 +- .../network/FhirLoggingInterceptor.java | 69 --- .../client/network/FhirRequestBuilder.java | 192 ++----- .../client/network/RetryInterceptor.java | 67 --- .../utils/client/FHIRToolingClientTest.java | 84 +-- .../client/network/ClientHeadersTest.java | 24 +- .../network/FhirRequestBuilderTest.java | 87 ++-- org.hl7.fhir.r5/pom.xml | 6 + .../hl7/fhir/r5/fhirpath/FHIRPathEngine.java | 11 +- .../TerminologyCacheManager.java | 12 +- .../client/TerminologyClientR5.java | 4 - .../r5/utils/client/FHIRToolingClient.java | 119 ++--- .../fhir/r5/utils/client/network/Client.java | 176 +++---- .../utils/client/network/ClientHeaders.java | 40 +- .../network/FhirLoggingInterceptor.java | 70 --- .../client/network/FhirRequestBuilder.java | 230 +++------ .../client/network/RetryInterceptor.java | 63 --- .../utils/client/FHIRToolingClientTest.java | 106 ++-- .../client/network/ClientHeadersTest.java | 25 +- .../network/FhirRequestBuilderTest.java | 92 ++-- .../hl7/fhir/utilities/http/HTTPHeader.java | 21 + .../fhir/utilities/http/HTTPHeaderUtil.java | 45 ++ .../hl7/fhir/utilities/http/HTTPRequest.java | 54 ++ .../hl7/fhir/utilities/http/HTTPResult.java | 49 +- .../http/ManagedFhirWebAccessor.java | 170 ++++++ .../fhir/utilities/http/ManagedWebAccess.java | 73 ++- .../utilities/http/ManagedWebAccessUtils.java | 17 + ...ssBuilder.java => ManagedWebAccessor.java} | 125 ++--- .../http/ManagedWebAccessorBase.java | 78 +++ .../fhir/utilities/http/SimpleHTTPClient.java | 27 +- .../http/okhttpimpl/LoggingInterceptor.java | 69 +++ .../http/okhttpimpl/ProxyAuthenticator.java | 24 + .../http/okhttpimpl}/RetryInterceptor.java | 8 +- .../fhir/utilities/i18n/I18nConstants.java | 7 +- .../hl7/fhir/utilities/npm/PackageClient.java | 11 +- .../fhir/utilities/settings/FhirSettings.java | 11 +- .../src/main/resources/Messages.properties | 12 +- .../fhir/utilities/http/HTTPHeaderTests.java | 20 + .../http/ManagedFhirWebAccessorTests.java | 40 ++ .../http/ManagedWebAccessAuthTests.java | 216 ++++++++ .../org/hl7/fhir/validation/ValidatorCli.java | 2 + .../instance/InstanceValidator.java | 42 +- .../instance/type/MeasureValidator.java | 11 + .../type/StructureDefinitionValidator.java | 46 +- .../hl7/fhir/validation/special/TxTester.java | 17 +- .../test/java/ManagedWebAccessAuthTests.java | 9 + pom.xml | 2 +- setup-and-cache-job-template.yml | 2 +- 81 files changed, 3871 insertions(+), 2983 deletions(-) create mode 100644 org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/ClientUtilsTest.java create mode 100644 org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClientTest.java create mode 100644 org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/FhirToolingClientTest.java create mode 100644 org.hl7.fhir.dstu3/src/test/java/org/hl7/fhir/dstu3/utils/client/network/ClientTest.java delete mode 100644 org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirLoggingInterceptor.java delete mode 100644 org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/RetryInterceptor.java create mode 100644 org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/FhirToolingClientTest.java create mode 100644 org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientHeadersTest.java create mode 100644 org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientTest.java create mode 100644 org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilderTest.java delete mode 100644 org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirLoggingInterceptor.java delete mode 100644 org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/RetryInterceptor.java delete mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirLoggingInterceptor.java delete mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/RetryInterceptor.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeader.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeaderUtil.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPRequest.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessUtils.java rename org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/{ManagedWebAccessBuilder.java => ManagedWebAccessor.java} (54%) create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessorBase.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/LoggingInterceptor.java create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/ProxyAuthenticator.java rename {org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network => org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl}/RetryInterceptor.java (94%) create mode 100644 org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/HTTPHeaderTests.java create mode 100644 org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessorTests.java create mode 100644 org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java create mode 100644 org.hl7.fhir.validation/src/test/java/ManagedWebAccessAuthTests.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2eb0724124..7ab9bcb1d0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,9 @@ * Fix problem validating json-property-key value pairs * Fix special case r5 loading of terminology to fix validation error on ExampleScenario * Improve handling of JSON format errors +* Fix slicing by type and profile to allow multiple options per slice +* List measure choices when a match by version can't be found +* Validate fhirpath expression in slice discriminators ## Other code changes diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/ICD11Generator.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/ICD11Generator.java index 2999ec0127..0e7a391d2e 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/ICD11Generator.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/ICD11Generator.java @@ -1,6 +1,5 @@ package org.hl7.fhir.convertors.misc; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; import java.util.HashSet; @@ -395,7 +394,7 @@ private CodeSystem makeEntityCodeSystem() { private JsonObject fetchJson(String source) throws IOException { - HTTPResult res = ManagedWebAccess.builder().withAccept("application/json").withHeader("API-Version", "v2").withHeader("Accept-Language", "en").get(source); + HTTPResult res = ManagedWebAccess.accessor().withHeader("API-Version", "v2").withHeader("Accept-Language", "en").get(source,"application/json"); res.checkThrowException(); return JsonParser.parseObject(res.getContent()); } diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java index cfe79e7733..13fb36c81c 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/VSACImporter.java @@ -32,6 +32,7 @@ import org.hl7.fhir.utilities.CSVReader; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; +import org.hl7.fhir.utilities.http.ManagedWebAccess; import org.hl7.fhir.utilities.json.model.JsonArray; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.model.JsonProperty; @@ -45,17 +46,16 @@ public VSACImporter() throws FHIRException, IOException { public static void main(String[] args) throws FHIRException, IOException, ParseException, URISyntaxException { VSACImporter self = new VSACImporter(); - self.process(args[0], args[1], args[2], "true".equals(args[3]), "true".equals(args[4])); + self.process(args[0], args[1], "true".equals(args[2]), "true".equals(args[3])); } - private void process(String source, String dest, String apiKey, boolean onlyNew, boolean onlyActive) throws FHIRException, IOException, URISyntaxException { + private void process(String source, String dest, boolean onlyNew, boolean onlyActive) throws FHIRException, IOException, URISyntaxException { CSVReader csv = new CSVReader(ManagedFileAccess.inStream(source)); csv.readHeaders(); Map errs = new HashMap<>(); + ManagedWebAccess.loadFromFHIRSettings(); FHIRToolingClient fhirToolingClient = new FHIRToolingClient("https://cts.nlm.nih.gov/fhir", "fhir/vsac"); - fhirToolingClient.setUsername("apikey"); - fhirToolingClient.setPassword(apiKey); fhirToolingClient.setTimeoutNormal(30000); fhirToolingClient.setTimeoutExpand(30000); @@ -121,6 +121,7 @@ private void process(String source, String dest, String apiKey, boolean onlyNew, oo.addIssue().setSeverity(IssueSeverity.ERROR).setCode(IssueType.EXCEPTION).setDiagnostics(errs.get(oid)).addLocation(oid); } new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "other", "OperationOutcome-vsac-errors.json")), oo); + System.out.println(); System.out.println("Done. " + i + " ValueSets in "+Utilities.describeDuration(System.currentTimeMillis() - tt)); } diff --git a/org.hl7.fhir.dstu2/pom.xml b/org.hl7.fhir.dstu2/pom.xml index 1dc4fd998b..881479b405 100644 --- a/org.hl7.fhir.dstu2/pom.xml +++ b/org.hl7.fhir.dstu2/pom.xml @@ -28,6 +28,12 @@ org.hl7.fhir.utilities + + org.projectlombok + lombok + provided + + org.fhir @@ -81,6 +87,24 @@ + + org.assertj + assertj-core + test + + + + com.squareup.okhttp3 + mockwebserver + true + test + + + com.squareup.okio + okio + true + test + com.fasterxml.jackson.core jackson-databind diff --git a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ClientUtils.java b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ClientUtils.java index e4261fd9c3..c8dcb0bfcf 100644 --- a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ClientUtils.java +++ b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ClientUtils.java @@ -33,42 +33,21 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URLConnection; -import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import org.apache.commons.codec.binary.Base64; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.Setter; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.http.Header; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpOptions; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.params.ConnRoutePNames; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; + import org.hl7.fhir.dstu2.formats.IParser; import org.hl7.fhir.dstu2.formats.IParser.OutputStyle; import org.hl7.fhir.dstu2.formats.JsonParser; @@ -84,8 +63,11 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.utilities.MimeType; import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.http.*; import org.hl7.fhir.utilities.settings.FhirSettings; +import javax.annotation.Nonnull; + /** * Helper class handling lower level HTTP transport concerns. TODO Document * methods. @@ -93,51 +75,35 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS * @author Claude Nanjo */ public class ClientUtils { - + protected static final String LOCATION_HEADER = "location"; + protected static final String CONTENT_LOCATION_HEADER = "content-location"; public static final String DEFAULT_CHARSET = "UTF-8"; - public static final String HEADER_LOCATION = "location"; + private static boolean debugging = false; - private HttpHost proxy; + @Getter + @Setter private int timeout = 5000; - private String username; - private String password; - private ToolingClientLogger logger; - private int retryCount; - private String userAgent; - private String acceptLang; - private String contentLang; - - public HttpHost getProxy() { - return proxy; - } - public void setProxy(HttpHost proxy) { - this.proxy = proxy; - } - - public int getTimeout() { - return timeout; - } - - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - public String getUsername() { - return username; - } + @Setter + @Getter + private ToolingClientLogger logger; - public void setUsername(String username) { - this.username = username; - } + @Setter + @Getter + private int retryCount; - public String getPassword() { - return password; - } + @Getter + @Setter + private String userAgent; + @Setter + private String acceptLanguage; + @Setter + private String contentLanguage; + private final TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; - public void setPassword(String password) { - this.password = password; + protected ManagedFhirWebAccessor getManagedWebAccessor() { + return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger); } public ResourceRequest issueOptionsRequest(URI optionsUri, String resourceFormat, @@ -146,8 +112,10 @@ public ResourceRequest issueOptionsRequest(URI optionsUr throw new FHIRException("Network Access is prohibited in this context"); } - HttpOptions options = new HttpOptions(optionsUri); - return issueResourceRequest(resourceFormat, options, timeoutLoading); + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.OPTIONS) + .withUrl(optionsUri.toString()); + return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading); } public ResourceRequest issueGetResourceRequest(URI resourceUri, String resourceFormat, @@ -155,17 +123,23 @@ public ResourceRequest issueGetResourceRequest(URI resou if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpGet httpget = new HttpGet(resourceUri); - return issueResourceRequest(resourceFormat, httpget, timeoutLoading); + + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.GET) + .withUrl(resourceUri.toString()); + return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading); } public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, - List
headers, int timeoutLoading) { + Iterable headers, int timeoutLoading) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpPut httpPut = new HttpPut(resourceUri); - return issueResourceRequest(resourceFormat, httpPut, payload, headers, timeoutLoading); + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.PUT) + .withUrl(resourceUri.toString()) + .withBody(payload); + return issueResourceRequest(resourceFormat, httpRequest, headers, timeoutLoading); } public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, @@ -173,17 +147,25 @@ public ResourceRequest issuePutRequest(URI resourceUri, if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpPut httpPut = new HttpPut(resourceUri); - return issueResourceRequest(resourceFormat, httpPut, payload, null, timeoutLoading); + + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.PUT) + .withUrl(resourceUri.toString()) + .withBody(payload); + return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading); } public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, - String resourceFormat, List
headers, int timeoutLoading) { + String resourceFormat, Iterable headers, int timeoutLoading) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpPost httpPost = new HttpPost(resourceUri); - return issueResourceRequest(resourceFormat, httpPost, payload, headers, timeoutLoading); + + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.POST) + .withUrl(resourceUri.toString()) + .withBody(payload); + return issueResourceRequest(resourceFormat, httpRequest, headers, timeoutLoading); } public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, @@ -195,30 +177,26 @@ public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpGet httpget = new HttpGet(resourceUri); - configureFhirRequest(httpget, resourceFormat); - HttpResponse response = sendRequest(httpget); - return unmarshalReference(response, resourceFormat); - } - private void setAuth(HttpRequest httpget) { - if (password != null) { - try { - byte[] b = Base64.encodeBase64((username + ":" + password).getBytes("ASCII")); - String b64 = new String(b, StandardCharsets.US_ASCII); - httpget.setHeader("Authorization", "Basic " + b64); - } catch (UnsupportedEncodingException e) { - } - } + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.GET) + .withUrl(resourceUri.toString()); + Iterable headers = getFhirHeaders(httpRequest, resourceFormat); + HTTPResult response = sendRequest(httpRequest.withHeaders(headers)); + return unmarshalReference(response, resourceFormat); } public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, int timeoutLoading) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpPost httpPost = new HttpPost(resourceUri); - configureFhirRequest(httpPost, resourceFormat); - HttpResponse response = sendPayload(httpPost, payload, proxy, timeoutLoading); + + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.POST) + .withUrl(resourceUri.toString()) + .withBody(payload); + Iterable headers = getFhirHeaders(httpRequest, resourceFormat); + HTTPResult response = sendPayload(httpRequest.withHeaders(headers)); return unmarshalFeed(response, resourceFormat); } @@ -226,9 +204,12 @@ public boolean issueDeleteRequest(URI resourceUri) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpDelete deleteRequest = new HttpDelete(resourceUri); - HttpResponse response = sendRequest(deleteRequest); - int responseStatusCode = response.getStatusLine().getStatusCode(); + + HTTPRequest request = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.DELETE) + .withUrl(resourceUri.toString()); + HTTPResult response = sendRequest(request); + int responseStatusCode = response.getCode(); boolean deletionSuccessful = false; if (responseStatusCode == 204) { deletionSuccessful = true; @@ -240,171 +221,120 @@ public boolean issueDeleteRequest(URI resourceUri) { * Request/Response Helper methods ***********************************************************/ - protected ResourceRequest issueResourceRequest(String resourceFormat, HttpUriRequest request, + protected ResourceRequest issueResourceRequest(String resourceFormat, HTTPRequest request, int timeoutLoading) { - return issueResourceRequest(resourceFormat, request, null, timeoutLoading); + return issueResourceRequest(resourceFormat, request, Collections.emptyList(), timeoutLoading); } - - /** - * @param resourceFormat - * @param options - * @return - */ - protected ResourceRequest issueResourceRequest(String resourceFormat, HttpUriRequest request, - byte[] payload, int timeoutLoading) { - return issueResourceRequest(resourceFormat, request, payload, null, timeoutLoading); - } - /** - * @param resourceFormat - * @param options - * @return + * Issue a resource request. + * @param resourceFormat the expected FHIR format + * @param request the request to be sent + * @param headers any additional headers to add + * + * @return A ResourceRequest object containing the requested resource */ - protected ResourceRequest issueResourceRequest(String resourceFormat, HttpUriRequest request, - byte[] payload, List
headers, int timeoutLoading) { + protected ResourceRequest issueResourceRequest(String resourceFormat, HTTPRequest request, + @Nonnull Iterable headers, int timeoutLoading) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - configureFhirRequest(request, resourceFormat, headers); - HttpResponse response = null; - if (request instanceof HttpEntityEnclosingRequest && payload != null) { - response = sendPayload((HttpEntityEnclosingRequestBase) request, payload, proxy, timeoutLoading); - } else if (request instanceof HttpEntityEnclosingRequest && payload == null) { - throw new EFhirClientException("PUT and POST requests require a non-null payload"); - } else { - response = sendRequest(request); + Iterable configuredHeaders = getFhirHeaders(request, resourceFormat, headers); + try { + + HTTPResult response = getManagedWebAccessor().httpCall(request.withHeaders(configuredHeaders)); + T resource = unmarshalReference(response, resourceFormat); + return new ResourceRequest(resource, response.getCode(), getLocationHeader(response.getHeaders())); + } catch (IOException ioe) { + throw new EFhirClientException("Error sending HTTP Post/Put Payload to " + "??" + ": " + ioe.getMessage(), + ioe); } - T resource = unmarshalReference(response, resourceFormat); - return new ResourceRequest(resource, response.getStatusLine().getStatusCode(), getLocationHeader(response)); } /** - * Method adds required request headers. TODO handle JSON request as well. + * Get required headers for FHIR requests. * - * @param request + * @param httpRequest the request + * @param format the expected format */ - protected void configureFhirRequest(HttpRequest request, String format) { - configureFhirRequest(request, format, null); + protected Iterable getFhirHeaders(HTTPRequest httpRequest, String format) { + return getFhirHeaders(httpRequest, format, null); } /** - * Method adds required request headers. TODO handle JSON request as well. + * Get required headers for FHIR requests. * - * @param request + * @param httpRequest the request + * @param format the expected format + * @param headers any additional headers to add */ - protected void configureFhirRequest(HttpRequest request, String format, List
headers) { + protected Iterable getFhirHeaders(HTTPRequest httpRequest, String format, Iterable headers) { + List configuredHeaders = new ArrayList<>(); if (!Utilities.noString(userAgent)) { - request.addHeader("User-Agent", userAgent); + configuredHeaders.add(new HTTPHeader("User-Agent", userAgent)); } - if (!Utilities.noString(acceptLang)) { - request.addHeader("Accept-Language", acceptLang); + if (!Utilities.noString(acceptLanguage)) { + configuredHeaders.add(new HTTPHeader("Accept-Language", acceptLanguage)); } - if (!Utilities.noString(contentLang)) { - request.addHeader("Content-Language", acceptLang); + if (!Utilities.noString(contentLanguage)) { + configuredHeaders.add(new HTTPHeader("Content-Language", acceptLanguage)); } - if (format != null) { - request.addHeader("Accept", format); - request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); - } + Iterable resourceFormatHeaders = getResourceFormatHeaders(httpRequest, format); + resourceFormatHeaders.forEach(configuredHeaders::add); + if (headers != null) { - for (Header header : headers) { - request.addHeader(header); - } + headers.forEach(configuredHeaders::add); } - setAuth(request); + return configuredHeaders; } - /** - * Method posts request payload - * - * @param request - * @param payload - * @return - */ - @SuppressWarnings({ "resource", "deprecation" }) - protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy, - int timeoutLoading) { - if (FhirSettings.isProhibitNetworkAccess()) { - throw new FHIRException("Network Access is prohibited in this context"); + protected static List 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)); } - HttpResponse response = null; - boolean ok = false; - long t = System.currentTimeMillis(); - int tryCount = 0; - while (!ok) { - try { - tryCount++; - HttpClient httpclient = new DefaultHttpClient(); - HttpParams params = httpclient.getParams(); - HttpConnectionParams.setConnectionTimeout(params, timeout); - HttpConnectionParams.setSoTimeout(params, timeout * timeoutLoading); - - if (proxy != null) { - httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); - } - request.setEntity(new ByteArrayEntity(payload)); - log(request); - response = httpclient.execute(request); - ok = true; - } catch (IOException ioe) { - System.out.println(ioe.getMessage() + " (" + (System.currentTimeMillis() - t) + "ms / " - + Utilities.describeSize(payload.length) + ")"); - if (tryCount <= retryCount || (tryCount < 3 && ioe instanceof org.apache.http.conn.ConnectTimeoutException)) { - ok = false; - } else { - throw new EFhirClientException("Error sending HTTP Post/Put Payload to " + "??" + ": " + ioe.getMessage(), - ioe); - } - } - } - return response; + return headers; } /** * - * @param request - * @param payload - * @return + * @param request The request to be sent + * @return The response from the server */ - protected HttpResponse sendRequest(HttpUriRequest request) { + protected HTTPResult sendRequest(HTTPRequest request) { if (FhirSettings.isProhibitNetworkAccess()) { throw new FHIRException("Network Access is prohibited in this context"); } - HttpResponse response = null; + HTTPResult response = null; try { - HttpClient httpclient = new DefaultHttpClient(); - log(request); - HttpParams params = httpclient.getParams(); - HttpConnectionParams.setConnectionTimeout(params, timeout); - HttpConnectionParams.setSoTimeout(params, timeout); - if (proxy != null) { - httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); - } - response = httpclient.execute(request); + + response = getManagedWebAccessor().httpCall(request); + return response; } catch (IOException ioe) { if (ClientUtils.debugging) { ioe.printStackTrace(); } throw new EFhirClientException("Error sending Http Request: " + ioe.getMessage(), ioe); } - return response; } /** * Unmarshals a resource from the response stream. * - * @param response - * @return + * @param response The response from the server + * @return The unmarshalled resource */ @SuppressWarnings("unchecked") - protected T unmarshalReference(HttpResponse response, String format) { + protected T unmarshalReference(HTTPResult response, String format) { T resource = null; OperationOutcome error = null; - byte[] cnt = log(response); - if (cnt != null) { + if (response.getContent() != null) { try { - resource = (T) getParser(format).parse(cnt); + resource = (T) getParser(format).parse(response.getContent()); if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) { error = (OperationOutcome) resource; } @@ -423,18 +353,18 @@ protected T unmarshalReference(HttpResponse response, Strin /** * Unmarshals Bundle from response stream. * - * @param response - * @return + * @param response The response from the server + * @return The unmarshalled Bundle */ - protected Bundle unmarshalFeed(HttpResponse response, String format) { + protected Bundle unmarshalFeed(HTTPResult response, String format) { Bundle feed = null; - byte[] cnt = log(response); - String contentType = response.getHeaders("Content-Type")[0].getValue(); + + String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type"); OperationOutcome error = null; try { - if (cnt != null) { + if (response.getContent() != null) { if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) { - Resource rf = getParser(format).parse(cnt); + Resource rf = getParser(format).parse(response.getContent()); if (rf instanceof Bundle) feed = (Bundle) rf; else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { @@ -455,21 +385,20 @@ else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { return feed; } - private boolean hasError(OperationOutcome oo) { + protected boolean hasError(OperationOutcome oo) { for (OperationOutcomeIssueComponent t : oo.getIssue()) if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL) return true; return false; } - protected String getLocationHeader(HttpResponse response) { - String location = null; - if (response.getHeaders("location").length > 0) {// TODO Distinguish between both cases if necessary - location = response.getHeaders("location")[0].getValue(); - } else if (response.getHeaders("content-location").length > 0) { - location = response.getHeaders("content-location")[0].getValue(); + protected static String getLocationHeader(Iterable headers) { + String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER); + + if (locationHeader != null) { + return locationHeader; } - return location; + return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER); } /***************************************************************** @@ -586,12 +515,17 @@ protected IParser getParser(String format) { public Bundle issuePostFeedRequest(URI resourceUri, Map parameters, String resourceName, Resource resource, String resourceFormat) throws IOException { - HttpPost httppost = new HttpPost(resourceUri); + + HTTPRequest httpRequest = new HTTPRequest() + .withMethod(HTTPRequest.HttpMethod.POST) + .withUrl(resourceUri.toString()); String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; - httppost.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary); - httppost.addHeader("Accept", resourceFormat); - configureFhirRequest(httppost, null); - HttpResponse response = sendPayload(httppost, encodeFormSubmission(parameters, resourceName, resource, boundary)); + List headers = new ArrayList<>(); + headers.add(new HTTPHeader("Content-Type", "multipart/form-data; boundary=" + boundary)); + headers.add(new HTTPHeader("Accept", resourceFormat)); + this.getFhirHeaders(httpRequest, null).forEach(headers::add); + + HTTPResult response = sendPayload(httpRequest.withBody(encodeFormSubmission(parameters, resourceName, resource, boundary)).withHeaders(headers)); return unmarshalFeed(response, resourceFormat); } @@ -622,80 +556,22 @@ private byte[] encodeFormSubmission(Map parameters, String resou } /** - * Method posts request payload + * Send an HTTP Post/Put Payload * - * @param request - * @param payload - * @return + * @param request The request to be sent + * @return The response from the server */ - protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) { - HttpResponse response = null; + protected HTTPResult sendPayload(HTTPRequest request) { + HTTPResult response = null; try { - log(request); - HttpClient httpclient = new DefaultHttpClient(); - request.setEntity(new ByteArrayEntity(payload)); - response = httpclient.execute(request); - log(response); + + response = getManagedWebAccessor().httpCall(request); } catch (IOException ioe) { throw new EFhirClientException("Error sending HTTP Post/Put Payload: " + ioe.getMessage(), ioe); } return response; } - private void log(HttpUriRequest request) { - if (logger != null) { - List headers = new ArrayList<>(); - for (Header h : request.getAllHeaders()) { - headers.add(h.toString()); - } - logger.logRequest(request.getMethod(), request.getURI().toString(), headers, null); - } - } - - private void log(HttpEntityEnclosingRequestBase request) { - if (logger != null) { - List headers = new ArrayList<>(); - for (Header h : request.getAllHeaders()) { - headers.add(h.toString()); - } - byte[] cnt = null; - InputStream s; - try { - s = request.getEntity().getContent(); - cnt = IOUtils.toByteArray(s); - s.close(); - } catch (Exception e) { - } - logger.logRequest(request.getMethod(), request.getURI().toString(), headers, cnt); - } - } - - private byte[] log(HttpResponse response) { - byte[] cnt = null; - try { - InputStream s = response.getEntity().getContent(); - cnt = IOUtils.toByteArray(s); - s.close(); - } catch (Exception e) { - } - if (logger != null) { - List headers = new ArrayList<>(); - for (Header h : response.getAllHeaders()) { - headers.add(h.toString()); - } - logger.logResponse(response.getStatusLine().toString(), headers, cnt, 0); - } - return cnt; - } - - public ToolingClientLogger getLogger() { - return logger; - } - - public void setLogger(ToolingClientLogger logger) { - this.logger = logger; - } - /** * Used for debugging * @@ -714,26 +590,5 @@ protected String writeInputStreamAsString(InputStream instream) { return value; } - public int getRetryCount() { - return retryCount; - } - - public void setRetryCount(int retryCount) { - this.retryCount = retryCount; - } - public String getUserAgent() { - return userAgent; - } - - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; - } - - public void setAcceptLanguage(String language) { - this.acceptLang = language; - } - public void setContentLanguage(String language) { - this.contentLang = language; - } } \ No newline at end of file diff --git a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClient.java b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClient.java index ff5900da4a..5f42d3d2d3 100644 --- a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClient.java +++ b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClient.java @@ -95,48 +95,21 @@ public class FHIRToolingClient extends FHIRBaseToolingClient { private HttpHost proxy; private int maxResultSetSize = -1;// _count private Conformance conf; - private ClientUtils utils = new ClientUtils(); + private ClientUtils utils = null; private int useCount; - // Pass enpoint for client - URI - public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException { - preferredResourceFormat = ResourceFormat.RESOURCE_XML; - utils.setUserAgent(userAgent); - detectProxy(); - initialize(baseServiceUrl); + protected ClientUtils getClientUtils() { + return new ClientUtils(); } - public FHIRToolingClient(String baseServiceUrl, String userAgent, String username, String password) - throws URISyntaxException { + // Pass enpoint for client - URI + public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException { preferredResourceFormat = ResourceFormat.RESOURCE_XML; + utils = getClientUtils(); utils.setUserAgent(userAgent); - utils.setUsername(username); - utils.setPassword(password); - detectProxy(); initialize(baseServiceUrl); } - public void configureProxy(String proxyHost, int proxyPort) { - utils.setProxy(new HttpHost(proxyHost, proxyPort)); - } - - public void detectProxy() { - String host = System.getenv(hostKey); - String port = System.getenv(portKey); - - if (host == null) { - host = System.getProperty(hostKey); - } - - if (port == null) { - port = System.getProperty(portKey); - } - - if (host != null && port != null) { - this.configureProxy(host, Integer.parseInt(port)); - } - } - public void initialize(String baseServiceUrl) throws URISyntaxException { base = baseServiceUrl; resourceAddress = new ResourceAddress(baseServiceUrl); @@ -286,11 +259,11 @@ public Resource update(Resource resource) { recordUse(); ResourceRequest result = null; try { - List
headers = null; + result = utils.issuePutRequest( resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()), utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), - withVer(getPreferredResourceFormat(), "1.0"), headers, timeoutOperation); + withVer(getPreferredResourceFormat(), "1.0"), null, timeoutOperation); result.addErrorStatus(410);// gone result.addErrorStatus(404);// unknown result.addErrorStatus(405); @@ -323,10 +296,12 @@ public T update(Class resourceClass, T resource, String recordUse(); ResourceRequest result = null; try { - List
headers = null; - result = utils.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), + result = utils.issuePutRequest( + resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), - withVer(getPreferredResourceFormat(), "1.0"), headers, timeoutOperation); + withVer(getPreferredResourceFormat(), "1.0"), + null, + timeoutOperation); result.addErrorStatus(410);// gone result.addErrorStatus(404);// unknown result.addErrorStatus(405); @@ -355,170 +330,7 @@ public T update(Class resourceClass, T resource, String return result.getPayload(); } -// -// public boolean delete(Class resourceClass, String id) { -// try { -// return utils.issueDeleteRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), proxy); -// } catch(Exception e) { -// throw new EFhirClientException("An error has occurred while trying to delete this resource", e); -// } -// -// } - -// -// public OperationOutcome create(Class resourceClass, T resource) { -// ResourceRequest resourceRequest = null; -// try { -// List
headers = null; -// resourceRequest = utils.issuePostRequest(resourceAddress.resolveGetUriFromResourceClass(resourceClass),utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), headers, proxy); -// resourceRequest.addSuccessStatus(201); -// if(resourceRequest.isUnsuccessfulRequest()) { -// throw new EFhirClientException("Server responded with HTTP error code " + resourceRequest.getHttpStatus(), (OperationOutcome)resourceRequest.getPayload()); -// } -// } catch(Exception e) { -// handleException("An error has occurred while trying to create this resource", e); -// } -// OperationOutcome operationOutcome = null;; -// try { -// operationOutcome = (OperationOutcome)resourceRequest.getPayload(); -// ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = -// ResourceAddress.parseCreateLocation(resourceRequest.getLocation()); -// OperationOutcomeIssueComponent issue = operationOutcome.addIssue(); -// issue.setSeverity(IssueSeverity.INFORMATION); -// issue.setUserData(ResourceAddress.ResourceVersionedIdentifier.class.toString(), -// resVersionedIdentifier); -// return operationOutcome; -// } catch(ClassCastException e) { -// // some server (e.g. grahams) returns the resource directly -// operationOutcome = new OperationOutcome(); -// OperationOutcomeIssueComponent issue = operationOutcome.addIssue(); -// issue.setSeverity(IssueSeverity.INFORMATION); -// issue.setUserData(ResourceRequest.class.toString(), -// resourceRequest.getPayload()); -// return operationOutcome; -// } -// } - -// -// public Bundle history(Calendar lastUpdate, Class resourceClass, String id) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceId(resourceClass, id, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history information for this resource", e); -// } -// return history; -// } - -// -// public Bundle history(Date lastUpdate, Class resourceClass, String id) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceId(resourceClass, id, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history information for this resource", e); -// } -// return history; -// } -// -// -// public Bundle history(Calendar lastUpdate, Class resourceClass) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceType(resourceClass, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history information for this resource type", e); -// } -// return history; -// } -// -// -// public Bundle history(Date lastUpdate, Class resourceClass) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceType(resourceClass, lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history information for this resource type", e); -// } -// return history; -// } -// -// -// public Bundle history(Class resourceClass) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceType(resourceClass, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history information for this resource type", e); -// } -// return history; -// } -// -// -// public Bundle history(Class resourceClass, String id) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForResourceId(resourceClass, id, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history information for this resource", e); -// } -// return history; -// } -// -// -// public Bundle history(Date lastUpdate) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForAllResources(lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history since last update",e); -// } -// return history; -// } -// -// -// public Bundle history(Calendar lastUpdate) { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForAllResources(lastUpdate, maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history since last update",e); -// } -// return history; -// } -// -// -// public Bundle history() { -// Bundle history = null; -// try { -// history = utils.issueGetFeedRequest(resourceAddress.resolveGetHistoryForAllResources(maxResultSetSize), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("An error has occurred while trying to retrieve history since last update",e); -// } -// return history; -// } -// -// -// public Bundle search(Class resourceClass, Map parameters) { -// Bundle searchResults = null; -// try { -// searchResults = utils.issueGetFeedRequest(resourceAddress.resolveSearchUri(resourceClass, parameters), withVer(getPreferredResourceFormat(), "1.0"), proxy); -// } catch (Exception e) { -// handleException("Error performing search with parameters " + parameters, e); -// } -// return searchResults; -// } -// -// -// public Bundle searchPost(Class resourceClass, T resource, Map parameters) { -// Bundle searchResults = null; -// try { -// searchResults = utils.issuePostFeedRequest(resourceAddress.resolveSearchUri(resourceClass, new HashMap()), parameters, "src", resource, getPreferredResourceFormat()); -// } catch (Exception e) { -// handleException("Error performing search with parameters " + parameters, e); -// } -// return searchResults; -// } + public Parameters operateType(Class resourceClass, String name, Parameters params) { recordUse(); @@ -588,107 +400,15 @@ public OperationOutcome validate(Class resourceClass, T return (OperationOutcome) result.getPayload(); } - /* - * change to meta operations - * - * public List getAllTags() { TagListRequest result = null; try { result - * = utils.issueGetRequestForTagList(resourceAddress.resolveGetAllTags(), - * withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception e) { - * handleException("An error has occurred while trying to retrieve all tags", - * e); } return result.getPayload(); } - * - * - * public List getAllTagsForResourceType(Class - * resourceClass) { TagListRequest result = null; try { result = - * utils.issueGetRequestForTagList(resourceAddress. - * resolveGetAllTagsForResourceType(resourceClass), - * withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception e) { - * handleException("An error has occurred while trying to retrieve tags for this resource type" - * , e); } return result.getPayload(); } - * - * - * public List getTagsForReference(Class - * resource, String id) { TagListRequest result = null; try { result = - * utils.issueGetRequestForTagList(resourceAddress.resolveGetTagsForReference( - * resource, id), withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception - * e) { - * handleException("An error has occurred while trying to retrieve tags for this resource" - * , e); } return result.getPayload(); } - * - * - * public List getTagsForResourceVersion(Class - * resource, String id, String versionId) { TagListRequest result = null; try { - * result = utils.issueGetRequestForTagList(resourceAddress. - * resolveGetTagsForResourceVersion(resource, id, versionId), - * withVer(getPreferredResourceFormat(), "1.0"), null, proxy); } catch (Exception e) { - * handleException("An error has occurred while trying to retrieve tags for this resource version" - * , e); } return result.getPayload(); } - * - * // // public boolean deleteTagsForReference(Class - * resourceClass, String id) { // try { // return - * utils.issueDeleteRequest(resourceAddress.resolveGetTagsForReference( - * resourceClass, id), proxy); // } catch(Exception e) { // - * handleException("An error has occurred while trying to retrieve tags for this resource version" - * , e); // throw new - * EFhirClientException("An error has occurred while trying to delete this resource" - * , e); // } // // } // // // public boolean - * deleteTagsForResourceVersion(Class resourceClass, String id, List - * tags, String version) { // try { // return - * utils.issueDeleteRequest(resourceAddress.resolveGetTagsForResourceVersion( - * resourceClass, id, version), proxy); // } catch(Exception e) { // - * handleException("An error has occurred while trying to retrieve tags for this resource version" - * , e); // throw new - * EFhirClientException("An error has occurred while trying to delete this resource" - * , e); // } // } - * - * - * public List createTags(List tags, - * Class resourceClass, String id) { TagListRequest request = null; try { - * request = - * utils.issuePostRequestForTagList(resourceAddress.resolveGetTagsForReference( - * resourceClass, id),utils.getTagListAsByteArray(tags, false, - * isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), null, - * proxy); request.addSuccessStatus(201); request.addSuccessStatus(200); - * if(request.isUnsuccessfulRequest()) { throw new - * EFhirClientException("Server responded with HTTP error code " + - * request.getHttpStatus()); } } catch(Exception e) { - * handleException("An error has occurred while trying to set tags for this resource" - * , e); } return request.getPayload(); } - * - * - * public List createTags(List tags, - * Class resourceClass, String id, String version) { TagListRequest request = - * null; try { request = utils.issuePostRequestForTagList(resourceAddress. - * resolveGetTagsForResourceVersion(resourceClass, id, - * version),utils.getTagListAsByteArray(tags, false, - * isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), null, - * proxy); request.addSuccessStatus(201); request.addSuccessStatus(200); - * if(request.isUnsuccessfulRequest()) { throw new - * EFhirClientException("Server responded with HTTP error code " + - * request.getHttpStatus()); } } catch(Exception e) { - * handleException("An error has occurred while trying to set the tags for this resource version" - * , e); } return request.getPayload(); } - * - * - * public List deleteTags(List tags, - * Class resourceClass, String id, String version) { TagListRequest request = - * null; try { request = utils.issuePostRequestForTagList(resourceAddress. - * resolveDeleteTagsForResourceVersion(resourceClass, id, - * version),utils.getTagListAsByteArray(tags, false, - * isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), null, - * proxy); request.addSuccessStatus(201); request.addSuccessStatus(200); - * if(request.isUnsuccessfulRequest()) { throw new - * EFhirClientException("Server responded with HTTP error code " + - * request.getHttpStatus()); } } catch(Exception e) { - * handleException("An error has occurred while trying to delete the tags for this resource version" - * , e); } return request.getPayload(); } - */ /** - * Helper method to prevent nesting of previously thrown EFhirClientExceptions - * - * @param e - * @throws EFhirClientException + * Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of + * EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the + * cause. + * + * @param message The EFhirClientException message. + * @param e The exception. + * @throws EFhirClientException representing the exception. */ protected void handleException(String message, Exception e) throws EFhirClientException { if (e instanceof EFhirClientException) { @@ -702,8 +422,8 @@ protected void handleException(String message, Exception e) throws EFhirClientEx * Helper method to determine whether desired resource representation is Json or * XML. * - * @param format - * @return + * @param format the format to check + * @return true if JSON, false if XML */ protected boolean isJson(String format) { boolean isJson = false; @@ -784,13 +504,13 @@ public Parameters translate(Parameters p) { public ValueSet expandValueset(ValueSet source, Parameters expParams) { recordUse(); - List
headers = null; + Parameters p = expParams == null ? new Parameters() : expParams.copy(); p.addParameter().setName("valueSet").setResource(source); ResourceRequest result = utils.issuePostRequest( resourceAddress.resolveOperationUri(ValueSet.class, "expand"), utils.getResourceAsByteArray(p, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), - headers, 4); + null, 4); result.addErrorStatus(410); // gone result.addErrorStatus(404); // unknown result.addErrorStatus(405); @@ -812,11 +532,11 @@ public ConceptMap initializeClosure(String name) { recordUse(); Parameters params = new Parameters(); params.addParameter().setName("name").setValue(new StringType(name)); - List
headers = null; + ResourceRequest result = utils.issuePostRequest( resourceAddress.resolveOperationUri(null, "closure", new HashMap()), utils.getResourceAsByteArray(params, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), - headers, timeoutNormal); + null, timeoutNormal); result.addErrorStatus(410);// gone result.addErrorStatus(404);// unknown result.addErrorStatus(405); @@ -835,11 +555,11 @@ public ConceptMap updateClosure(String name, Coding coding) { Parameters params = new Parameters(); params.addParameter().setName("name").setValue(new StringType(name)); params.addParameter().setName("concept").setValue(coding); - List
headers = null; + ResourceRequest result = utils.issuePostRequest( resourceAddress.resolveOperationUri(null, "closure", new HashMap()), utils.getResourceAsByteArray(params, false, isJson(getPreferredResourceFormat())), withVer(getPreferredResourceFormat(), "1.0"), - headers, timeoutOperation); + null, timeoutOperation); result.addErrorStatus(410);// gone result.addErrorStatus(404);// unknown result.addErrorStatus(405); @@ -861,22 +581,6 @@ public void setTimeout(int timeout) { utils.setTimeout(timeout); } - public String getUsername() { - return utils.getUsername(); - } - - public void setUsername(String username) { - utils.setUsername(username); - } - - public String getPassword() { - return utils.getPassword(); - } - - public void setPassword(String password) { - utils.setPassword(password); - } - public Parameters getTerminologyCapabilities() { return (Parameters) utils .issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), withVer(getPreferredResourceFormat(), "1.0"), timeoutNormal) diff --git a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceAddress.java b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceAddress.java index 7ff64f2bcf..e58b6fdaa0 100644 --- a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceAddress.java +++ b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceAddress.java @@ -233,9 +233,8 @@ public URI resolveMetadataTxCaps() { /** * For now, assume this type of location header structure. Generalize later: * http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1 - * - * @param serviceBase - * @param locationHeader + * + * @param locationResponseHeader */ public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) { Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY); diff --git a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceRequest.java b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceRequest.java index 3b337373d6..885112ba47 100644 --- a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceRequest.java +++ b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/utils/client/ResourceRequest.java @@ -32,11 +32,15 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import java.util.ArrayList; import java.util.List; +import lombok.Getter; import org.hl7.fhir.dstu2.model.Resource; public class ResourceRequest { + @Getter private T payload; + @Getter private int httpStatus = -1; + @Getter private String location; private List successfulStatuses = new ArrayList(); private List errorStatuses = new ArrayList(); @@ -67,14 +71,6 @@ public ResourceRequest(T payload, int httpStatus, int successfulStatus, String l this.location = location; } - public int getHttpStatus() { - return httpStatus; - } - - public T getPayload() { - return payload; - } - public T getReference() { T payloadResource = null; if (payload != null) { @@ -99,7 +95,4 @@ public void addErrorStatus(int status) { this.errorStatuses.add(status); } - public String getLocation() { - return location; - } } \ No newline at end of file diff --git a/org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/ClientUtilsTest.java b/org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/ClientUtilsTest.java new file mode 100644 index 0000000000..f659af3121 --- /dev/null +++ b/org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/ClientUtilsTest.java @@ -0,0 +1,350 @@ +package org.hl7.fhir.dstu2.utils.client; + + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.hl7.fhir.dstu2.formats.JsonParser; +import org.hl7.fhir.dstu2.model.*; +import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPHeaderUtil; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +public class ClientUtilsTest { + + public static final String DUMMY_LOCATION = "http://myhost/Patient/1"; + public static final int TIMEOUT = 5000; + private MockWebServer server; + private HttpUrl serverUrl; + private ClientUtils clientUtils; + + private final Address address = new Address() + .setCity("Toronto") + .setState("Ontario") + .setCountry("Canada"); + private final HumanName humanName = new HumanName() + .addGiven("Mark") + .addFamily("Iantorno"); + private final Patient patient = new Patient() + .addName(humanName) + .addAddress(address) + .setGender(Enumerations.AdministrativeGender.MALE); + + @BeforeEach + void setup() { + setupMockServer(); + clientUtils = new ClientUtils(); + } + + void setupMockServer() { + server = new MockWebServer(); + serverUrl = server.url("/v1/endpoint"); + } + + @Test + @DisplayName("Test resource format headers are added correctly.") + void addResourceFormatHeadersGET() { + + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET); + + Iterable headers = ClientUtils.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.assertNull(headersMap.get("Content-Type"), "Content-Type header null."); + + + } + + @Test + @DisplayName("Test resource format headers are added correctly (POST).") + void addResourceFormatHeadersPOST() { + + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST); + + Iterable headers = ClientUtils.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=" + ClientUtils.DEFAULT_CHARSET, headersMap.get("Content-Type").get(0), + "Content-Type header not populated with expected value \"" + testFormat + ";charset=" + ClientUtils.DEFAULT_CHARSET + "\"."); + + + } + + @Test + public void testResourceFormatHeaders_GET() throws IOException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody(new String(generateResourceBytes(patient))) + .addHeader("Content-Type", ResourceFormat.RESOURCE_JSON) + ); + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT); + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader("Accept")).isEqualTo("application/json+fhir"); + } + + @Test + public void testResourceFormatHeaders_POST() throws IOException, InterruptedException { + byte[] payload = generateResourceBytes(patient); + server.enqueue( + new MockResponse() + .setBody(new String(payload)) + .addHeader("Content-Type", "application/json+fhir") + .setResponseCode(201) + ); + ResourceRequest resourceRequest = clientUtils.issuePostRequest(serverUrl.uri(), payload, "application/json+fhir", TIMEOUT); + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader("Accept")).isEqualTo("application/json+fhir"); + } + + @Test + public void testResourceRequest() { + ResourceRequest request = new ResourceRequest<>(new Patient(), 200, "location"); + request.addSuccessStatus(200); + assertTrue(request.getPayload().equalsDeep(new Patient())); + assertThat(request.getHttpStatus()).isEqualTo(200); + assertThat(request.getLocation()).isEqualTo("location"); + assertTrue(request.isSuccessfulRequest()); + assertFalse(request.isUnsuccessfulRequest()); + } + + @Test + public void testIssueGetResourceRequest() throws IOException { + server.enqueue( + new MockResponse() + .setBody(new String(generateResourceBytes(patient))) + .addHeader("Content-Type", "application/json+fhir") + .addHeader("Location", DUMMY_LOCATION) + ); + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT); + resourceRequest.addSuccessStatus(200); + assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION); + assertTrue(resourceRequest.isSuccessfulRequest()); + assertTrue(patient.equalsDeep(resourceRequest.getPayload())); + } + + @Test + void testIssueGetResourceRequest_withContentLocation() throws IOException { + server.enqueue( + new MockResponse() + .setBody(new String(generateResourceBytes(patient))) + .addHeader("Content-Type", "application/json+fhir") + .addHeader("Content-Location", DUMMY_LOCATION) + ); + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT); + + assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION); + } + + @Test + @DisplayName("Test that getLocationHeader returns 'location' header when both 'location' and 'content-location' are set.") + void testIssueGetResourceRequest_ReturnsLocationHeaderWhenBothSet() throws IOException { + server.enqueue( + new MockResponse() + .setBody(new String(generateResourceBytes(patient))) + .addHeader("Content-Type", "application/json+fhir") + .addHeader("Location", DUMMY_LOCATION) + .addHeader("Content-Location", "Wrong wrong wrong") + ); + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT); + assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION); + } + + @Test + @DisplayName("Test that getLocationHeader returns 'null' header when neither 'location' or 'content-location' are set.") + void testIssueGetResourceRequest_ReturnsNullWhenNoHeadersSet() throws IOException { + server.enqueue( + new MockResponse() + .setBody(new String(generateResourceBytes(patient))) + .addHeader("Content-Type", "application/json+fhir") + ); + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT); + assertThat(resourceRequest.getLocation()).isNull(); + } + + @Test + public void testIssuePostRequest() throws IOException, InterruptedException { + byte[] payload = generateResourceBytes(patient); + server.enqueue( + new MockResponse() + .setBody(new String(payload)) + .addHeader("Content-Type", "application/json+fhir") + .addHeader("Location", DUMMY_LOCATION) + .setResponseCode(201) + ); + ResourceRequest resourceRequest = clientUtils.issuePostRequest(serverUrl.uri(), payload, "application/json+fhir", TIMEOUT); + resourceRequest.addSuccessStatus(201); + + RecordedRequest recordedRequest = server.takeRequest(); + assertArrayEquals(payload, recordedRequest.getBody().readByteArray(), + "PUT request payload does not match send data."); + assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION); + assertTrue(resourceRequest.isSuccessfulRequest()); + assertTrue(patient.equalsDeep(resourceRequest.getPayload())); + } + + @Test + public void testIssuePutRequest() throws IOException, InterruptedException { + byte[] payload = generateResourceBytes(patient); + server.enqueue( + new MockResponse() + .setBody(new String(payload)) + .addHeader("Content-Type", "application/json+fhir") + .addHeader("Location", DUMMY_LOCATION) + .setResponseCode(200) + ); + ResourceRequest resourceRequest = clientUtils.issuePutRequest(serverUrl.uri(), payload, "application/json+fhir", TIMEOUT); + resourceRequest.addSuccessStatus(200); + RecordedRequest recordedRequest = server.takeRequest(); + assertArrayEquals(payload, recordedRequest.getBody().readByteArray(), + "PUT request payload does not match send data."); + + assertThat(resourceRequest.getLocation()).isEqualTo(DUMMY_LOCATION); + assertTrue(resourceRequest.isSuccessfulRequest()); + assertTrue(patient.equalsDeep(resourceRequest.getPayload())); + } + + @Test + public void testIssueDeleteRequest() throws IOException { + server.enqueue( + new MockResponse().addHeader("Location", DUMMY_LOCATION).setResponseCode(204) + ); + boolean success = clientUtils.issueDeleteRequest(serverUrl.uri()); + assertTrue(success); + } + + @Test + public void testIssueDeleteRequest_fail() throws IOException, InterruptedException { + server.enqueue( + new MockResponse().addHeader("Location", DUMMY_LOCATION).setResponseCode(500) + ); + boolean success = clientUtils.issueDeleteRequest(serverUrl.uri()); + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo("DELETE"); + assertThat(recordedRequest.getRequestUrl().uri()).isEqualTo(serverUrl.uri()); + assertFalse(success); + } + + @Test + @DisplayName("test logger works") + public void testLogger() throws IOException, InterruptedException { + byte[] payload = generateResourceBytes(patient); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody(new String(payload)) + ); + ToolingClientLogger mockLogger = Mockito.mock(ToolingClientLogger.class); + clientUtils.setLogger(mockLogger); + clientUtils.issuePostRequest(serverUrl.uri(), payload, "application/json+fhir" + , null, TIMEOUT); + server.takeRequest(); + Mockito.verify(mockLogger, Mockito.times(1)) + .logRequest(Mockito.anyString(), Mockito.anyString(), Mockito.anyList(), Mockito.any()); + Mockito.verify(mockLogger, Mockito.times(1)) + .logResponse(Mockito.anyString(), Mockito.anyList(), Mockito.any(), Mockito.anyLong()); + } + + @Test + @DisplayName("Test that FATAL issue severity triggers error.") + void hasErrorTestFatal() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.FATAL)); + Assertions.assertTrue(clientUtils.hasError(outcome), "Error check not triggered for FATAL issue severity."); + } + + @Test + @DisplayName("Test that ERROR issue severity triggers error.") + void hasErrorTestError() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.ERROR)); + Assertions.assertTrue(clientUtils.hasError(outcome), "Error check not triggered for ERROR issue severity."); + } + + @Test + @DisplayName("Test that no FATAL or ERROR issue severity does not trigger error.") + void hasErrorTestNoErrors() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + Assertions.assertFalse(clientUtils.hasError(outcome), "Error check triggered unexpectedly."); + } + + @Test + @DisplayName("GET request, test client retries after timeout failure.") + void test_get_retries_with_timeout() throws IOException, URISyntaxException { + int failedAttempts = new Random().nextInt(5) + 1; + System.out.println("Simulating <" + failedAttempts + "> failed connections (timeouts) before success."); + for (int i = 0; i < failedAttempts; i++) { + server.enqueue(new MockResponse().setHeadersDelay(TIMEOUT * 10, TimeUnit.MILLISECONDS) + .setBody(new String(generateResourceBytes(patient)))); + } + server.enqueue(new MockResponse().setBody(new String(generateResourceBytes(patient)))); + clientUtils.setRetryCount(failedAttempts + 1); + + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir" + , TIMEOUT); + resourceRequest.addSuccessStatus(200); + Assertions.assertTrue(resourceRequest.isSuccessfulRequest()); + Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()), + "GET request returned resource does not match expected."); + } + + @Test + @DisplayName("GET request, test client retries after bad response.") + void test_get_retries_with_unsuccessful_response() throws IOException, URISyntaxException { + int failedAttempts = new Random().nextInt(5) + 1; + System.out.println("Simulating <" + failedAttempts + "> failed connections (bad response codes) before success."); + for (int i = 0; i < failedAttempts; i++) { + server.enqueue(new MockResponse().setResponseCode(400 + i).setBody(new String(generateResourceBytes(patient)))); + } + server.enqueue(new MockResponse().setBody(new String(generateResourceBytes(patient)))); + clientUtils.setRetryCount(failedAttempts + 1); + + ResourceRequest resourceRequest = clientUtils.issueGetResourceRequest(serverUrl.uri(), "application/json+fhir", TIMEOUT); + resourceRequest.addSuccessStatus(200); + Assertions.assertTrue(resourceRequest.isSuccessfulRequest()); + Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()), + "GET request returned resource does not match expected."); + } + + + byte[] generateResourceBytes(Resource resource) throws IOException { + return new JsonParser().composeBytes(resource); + } +} diff --git a/org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClientTest.java b/org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClientTest.java new file mode 100644 index 0000000000..587fdb9f29 --- /dev/null +++ b/org.hl7.fhir.dstu2/src/test/java/org/hl7/fhir/dstu2/utils/client/FHIRToolingClientTest.java @@ -0,0 +1,166 @@ +package org.hl7.fhir.dstu2.utils.client; + +import org.hl7.fhir.dstu2.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.*; + +public class FHIRToolingClientTest { + + private final Address address = new Address() + .setCity("Toronto") + .setState("Ontario") + .setCountry("Canada"); + private final HumanName humanName = new HumanName() + .addGiven("Mark") + .addFamily("Iantorno"); + private final Patient patient = new Patient() + .addName(humanName) + .addAddress(address) + .setGender(Enumerations.AdministrativeGender.MALE); + + private ClientUtils mockClientUtils; + private FHIRToolingClient toolingClient; + + @Captor + private ArgumentCaptor uriArgumentCaptor; + + @BeforeEach + public void beforeEach() throws URISyntaxException { + MockitoAnnotations.openMocks(this); + mockClientUtils = Mockito.mock(ClientUtils.class); + + toolingClient = new FHIRToolingClient("http://dummy-base-url.com", "dummy-user-agent") { + @Override + protected ClientUtils getClientUtils() { + return mockClientUtils; + } + }; + + /* + Need to reset here. When initialized, the client makes a call to getConformanceStatementQuick, which messes with + our expected calls. + */ + reset(mockClientUtils); + } + + @Test + public void testGetTerminologyCapabilities() throws URISyntaxException { + + Parameters expectedCapabilities = new Parameters(); + expectedCapabilities.addParameter().setName("name").setValue(new StringType("dummyValue")); + + when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(expectedCapabilities, 200, "location")); + Parameters actualCapabilities = toolingClient.getTerminologyCapabilities(); + + Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()); + assertThat(actualCapabilities).isEqualTo(expectedCapabilities); + + assertThat(uriArgumentCaptor.getValue()).isEqualTo(new URI("http://dummy-base-url.com/metadata?mode=terminology")); + } + + @Test + public void testGetConformanceStatement() throws URISyntaxException { + + Conformance expectedConformance = new Conformance(); + expectedConformance.setCopyright("dummyCopyright"); + when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(expectedConformance, 200, "location")); + Conformance actualConformance = toolingClient.getConformanceStatement(); + + Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()); + assertThat(actualConformance).isEqualTo(expectedConformance); + + assertThat(uriArgumentCaptor.getValue()).isEqualTo(new URI("http://dummy-base-url.com/metadata")); + } + + @Test + public void testGetConformanceStatementQuick() throws URISyntaxException { + + Conformance expectedConformance = new Conformance(); + expectedConformance.setCopyright("dummyCopyright"); + when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(expectedConformance, 200, "location")); + Conformance actualConformance = toolingClient.getConformanceStatementQuick(); + + Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()); + assertThat(actualConformance).isEqualTo(expectedConformance); + + assertThat(uriArgumentCaptor.getValue()).isEqualTo(new URI("http://dummy-base-url.com/metadata?_summary=true")); + } + + @Test + void testRead() { + when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(patient, 200, "location")); + Patient actualPatient = toolingClient.read(Patient.class, "id"); + + Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()); + assertThat(actualPatient).isEqualTo(patient); + + assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/id"); + } + + @Test + void testVRead() { + when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(patient, 200, "location")); + Patient actualPatient = toolingClient.vread(Patient.class, "id", "version"); + + Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()); + assertThat(actualPatient).isEqualTo(patient); + + assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/id/_history/version"); + } + + @Test + void testCanonical() { + Bundle bundle = new Bundle(); + bundle.addEntry().setResource(patient); + when(mockClientUtils.issueGetResourceRequest(uriArgumentCaptor.capture(), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(bundle, 200, "location")); + Patient actualPatient = toolingClient.getCanonical(Patient.class, "canonicalURL"); + + Mockito.verify(mockClientUtils).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()); + assertThat(actualPatient).isEqualTo(patient); + + assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient?url=canonicalURL"); + } + + @Test + void testUpdate() { + final byte[] dummyBytes = "dummyBytes".getBytes(); + when(mockClientUtils.getResourceAsByteArray(any(Patient.class), anyBoolean(), anyBoolean())).thenReturn(dummyBytes); + when(mockClientUtils.issuePutRequest(uriArgumentCaptor.capture(), Mockito.any(byte[].class), Mockito.anyString(), Mockito.isNull(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(patient, 200, "location")); + Patient actualPatient = toolingClient.update(Patient.class, patient, "id"); + + Mockito.verify(mockClientUtils).issuePutRequest(ArgumentMatchers.any(URI.class), Mockito.any(byte[].class), ArgumentMatchers.anyString(), ArgumentMatchers.isNull(),ArgumentMatchers.anyInt()); + assertThat(actualPatient).isEqualTo(patient); + + assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/id"); + } + + @Test + void testValidate() { + final byte[] dummyBytes = "dummyBytes".getBytes(); + final OperationOutcome expectedOutcome = new OperationOutcome(); + OperationOutcome.OperationOutcomeIssueComponent issueComponent = expectedOutcome.addIssue(); + issueComponent.setSeverity(OperationOutcome.IssueSeverity.ERROR); + when(mockClientUtils.getResourceAsByteArray(any(Patient.class), anyBoolean(), anyBoolean())).thenReturn(dummyBytes); + when(mockClientUtils.issuePostRequest(uriArgumentCaptor.capture(), Mockito.any(byte[].class), Mockito.anyString(), Mockito.anyInt())) + .thenReturn(new ResourceRequest<>(expectedOutcome, 200, "location")); + + OperationOutcome actualOutcome = toolingClient.validate(Patient.class, patient, "id"); + assertThat(actualOutcome).isEqualTo(expectedOutcome); + + assertThat(uriArgumentCaptor.getValue().toString()).isEqualTo("http://dummy-base-url.com/Patient/$validate/id"); + } +} diff --git a/org.hl7.fhir.dstu3/pom.xml b/org.hl7.fhir.dstu3/pom.xml index 8f61071b87..aef8d1dccc 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 fe6aec345f..220c97cd6f 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 @@ -3,18 +3,15 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import lombok.Getter; +import lombok.Setter; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.CapabilityStatement; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.ConceptMap; -import org.hl7.fhir.dstu3.model.ExpansionProfile; import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent; @@ -31,8 +28,7 @@ import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.Utilities; -import okhttp3.Headers; -import okhttp3.internal.http2.Header; +import org.hl7.fhir.utilities.http.HTTPHeader; /** * Very Simple RESTful client. This is purely for use in the standalone @@ -72,13 +68,17 @@ public class FHIRToolingClient extends FHIRBaseToolingClient { private int maxResultSetSize = -1;//_count private CapabilityStatement capabilities; private Client client = new Client(); - private ArrayList
headers = new ArrayList<>(); - private String username; - private String password; + private List headers = new ArrayList<>(); + @Setter + @Getter private String userAgent; private EnumSet allowedVersions; - private String acceptLang; - private String contentLang; + @Setter + @Getter + private String acceptLanguage; + @Setter + private String contentLanguage; + @Getter private int useCount; //Pass endpoint for client - URI @@ -150,7 +150,7 @@ public Parameters getTerminologyCapabilities() { try { capabilities = (Parameters) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "TerminologyCapabilities", timeoutNormal).getReference(); } catch (Exception e) { @@ -164,7 +164,7 @@ public CapabilityStatement getCapabilitiesStatement() { try { conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "CapabilitiesStatement", timeoutNormal).getReference(); } catch (Exception e) { @@ -178,7 +178,7 @@ public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientExc try { capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "CapabilitiesStatement-Quick", timeoutNormal).getReference(); } catch (Exception e) { @@ -193,7 +193,7 @@ public T read(Class resourceClass, String id) {//TODO Ch try { result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "Read " + resourceClass.getName() + "/" + id, timeoutNormal); if (result.isUnsuccessfulRequest()) { @@ -211,7 +211,7 @@ public T vread(Class resourceClass, String id, String ve try { result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, timeoutNormal); if (result.isUnsuccessfulRequest()) { @@ -229,7 +229,7 @@ public T getCanonical(Class resourceClass, String canoni try { result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "Read " + resourceClass.getName() + "?url=" + canonicalURL, timeoutNormal); if (result.isUnsuccessfulRequest()) { @@ -253,7 +253,7 @@ public Resource update(Resource resource) { result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(true), "Update " + resource.fhirType() + "/" + resource.getId(), timeoutOperation); if (result.isUnsuccessfulRequest()) { @@ -281,7 +281,7 @@ public T update(Class resourceClass, T resource, String result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(true), "Update " + resource.fhirType() + "/" + id, timeoutOperation); if (result.isUnsuccessfulRequest()) { @@ -320,13 +320,13 @@ public Parameters operateType(Class resourceClass, Strin if (client.getLogger() != null) { client.getLogger().logRequest("POST", url.toString(), null, body); } - result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(), + result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(true), "POST " + resourceClass.getName() + "/$" + name, timeoutLong); } else { if (client.getLogger() != null) { client.getLogger().logRequest("GET", url.toString(), null, null); } - result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, timeoutLong); + result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(false), "GET " + resourceClass.getName() + "/$" + name, timeoutLong); } if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -363,7 +363,7 @@ public OperationOutcome validate(Class resourceClass, T try { result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(), + withVer(getPreferredResourceFormat(), "3.0"), generateHeaders(true), "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -375,10 +375,14 @@ public OperationOutcome validate(Class resourceClass, T } /** - * Helper method to prevent nesting of previously thrown EFhirClientExceptions + * Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of + * EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the + * cause. * - * @param e - * @throws EFhirClientException + + * @param message The EFhirClientException message. + * @param e The exception + * @throws EFhirClientException EFhirClientException representing the exception. */ protected void handleException(String message, Exception e) throws EFhirClientException { if (e instanceof EFhirClientException) { @@ -389,11 +393,11 @@ protected void handleException(String message, Exception e) throws EFhirClientEx } /** - * Helper method to determine whether desired resource representation - * is Json or XML. + * Helper method to determine whether desired resource representation is Json or + * XML. * - * @param format - * @return + * @param format the format + * @return true if the format is JSON, false otherwise */ protected boolean isJson(String format) { boolean isJson = false; @@ -420,7 +424,7 @@ public Parameters lookupCode(Map params) { try { result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(false), "CodeSystem/$lookup", timeoutNormal); } catch (IOException e) { @@ -439,7 +443,7 @@ public Parameters lookupCode(Parameters p) { result = client.issuePostRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(true), "CodeSystem/$lookup", timeoutNormal); } catch (IOException e) { @@ -458,7 +462,7 @@ public Parameters transform(Parameters p) { result = client.issuePostRequest(resourceAddress.resolveOperationUri(ConceptMap.class, "transform"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(true), "ConceptMap/$transform", timeoutNormal); } catch (IOException e) { @@ -479,7 +483,7 @@ public ValueSet expandValueset(ValueSet source, Parameters expParams) { result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), withVer(getPreferredResourceFormat(), "3.0"), - generateHeaders(), + generateHeaders(true), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand); if (result.isUnsuccessfulRequest()) { @@ -504,7 +508,7 @@ public ConceptMap initializeClosure(String name) { 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()) { @@ -526,7 +530,7 @@ public ConceptMap updateClosure(String name, Coding coding) { 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()) { @@ -538,21 +542,6 @@ public ConceptMap updateClosure(String name, Coding coding) { return result == null ? null : (ConceptMap) result.getPayload(); } - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } public long getTimeout() { return client.getTimeout(); @@ -578,48 +567,27 @@ public void setRetryCount(int retryCount) { client.setRetryCount(retryCount); } - public void setClientHeaders(ArrayList
headers) { - this.headers = headers; + public void setClientHeaders(Iterable headers) { + this.headers =new ArrayList<>(); + headers.forEach(this.headers::add); } - private Headers generateHeaders() { - Headers.Builder builder = new Headers.Builder(); - // Add basic auth header if it exists - if (basicAuthHeaderExists()) { - builder.add(getAuthorizationHeader().toString()); - } + private Iterable generateHeaders(boolean hasBody) { // Add any other headers - if(this.headers != null) { - this.headers.forEach(header -> builder.add(header.toString())); - } + List headers = new ArrayList<>(this.headers); if (!Utilities.noString(userAgent)) { - builder.add("User-Agent: "+userAgent); - } - if (!Utilities.noString(acceptLang)) { - builder.add("Accept-Language: "+acceptLang); - } - if (!Utilities.noString(contentLang)) { - builder.add("Content-Language: "+contentLang); + headers.add(new HTTPHeader("User-Agent",userAgent)); } - return builder.build(); - } - public boolean basicAuthHeaderExists() { - return (username != null) && (password != null); - } + if (!Utilities.noString(acceptLanguage)) { + headers.add(new HTTPHeader("Accept-Language", acceptLanguage)); + } - public Header getAuthorizationHeader() { - String usernamePassword = username + ":" + password; - String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes()); - return new Header("Authorization", "Basic " + base64usernamePassword); - } - - public String getUserAgent() { - return userAgent; - } + if (hasBody && !Utilities.noString(contentLanguage)) { + headers.add(new HTTPHeader("Content-Language",contentLanguage)); + } - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; + return headers; } public String getServerVersion() { @@ -627,17 +595,6 @@ public String getServerVersion() { return capabilities == null ? null : capabilities.getSoftware().getVersion(); } - public void setAcceptLanguage(String lang) { - this.acceptLang = lang; - } - public void setContentLanguage(String lang) { - this.contentLang = lang; - } - - public int getUseCount() { - return useCount; - } - private void recordUse() { useCount++; } 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 a0635aadf8..a27f64b88f 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 @@ 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.utils.client.EFhirClientException; import org.hl7.fhir.utilities.ToolingClientLogger; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPRequest; public class Client { @@ -61,21 +60,22 @@ public ResourceRequest issueOptionsRequest(URI optionsUr String resourceFormat, String message, long timeout) throws IOException { - Request.Builder request = new Request.Builder() - .method("OPTIONS", null) - .url(optionsUri.toURL()); - return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(optionsUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.OPTIONS); + + return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout); } public ResourceRequest issueGetResourceRequest(URI resourceUri, String resourceFormat, - Headers headers, + Iterable headers, String message, long timeout) throws IOException { - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()); - + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } @@ -87,21 +87,21 @@ public ResourceRequest issuePutRequest(URI resourceUri, 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"); - RequestBody body = RequestBody.create(payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .put(body); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.PUT) + .withBody(payload); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } @@ -110,36 +110,36 @@ public ResourceRequest issuePostRequest(URI resourceUri, 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"); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .post(body); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } public boolean issueDeleteRequest(URI resourceUri) throws IOException { - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .delete(); - return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest(); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.DELETE); + return executeFhirRequest(request, null, Collections.emptyList(), null, retryCount, timeout).isSuccessfulRequest(); } public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException { - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()); - - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle issuePostFeedRequest(URI resourceUri, @@ -149,12 +149,13 @@ public Bundle issuePostFeedRequest(URI resourceUri, String resourceFormat) throws IOException { String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; byte[] payload = ByteUtils.encodeFormSubmission(parameters, resourceName, resource, boundary); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .post(body); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload); + + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle postBatchRequest(URI resourceUri, @@ -163,17 +164,17 @@ public Bundle postBatchRequest(URI resourceUri, String message, int timeout) throws IOException { if (payload == null) throw new EFhirClientException("POST requests require a non-null payload"); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .post(body); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout); } - public 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 +183,23 @@ public Bundle executeBundleRequest(Request.Builder request, .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/ClientHeaders.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/ClientHeaders.java index c235c7b199..0d530b00ab 100644 --- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/ClientHeaders.java +++ b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/ClientHeaders.java @@ -5,8 +5,7 @@ import java.util.stream.Collectors; import org.hl7.fhir.exceptions.FHIRException; - -import okhttp3.internal.http2.Header; +import org.hl7.fhir.utilities.http.HTTPHeader; /** * Generic Implementation of Client Headers. @@ -16,30 +15,30 @@ */ public class ClientHeaders { - private final ArrayList
headers; + private final ArrayList headers; public ClientHeaders() { this.headers = new ArrayList<>(); } - public ClientHeaders(ArrayList
headers) { + public ClientHeaders(ArrayList headers) { this.headers = headers; } - public ArrayList
headers() { + public ArrayList headers() { return headers; } /** * Add a header to the list of stored headers for network operations. * - * @param header {@link Header} to add to the list. + * @param header {@link HTTPHeader} to add to the list. * @throws FHIRException if the header being added is a duplicate. */ - public ClientHeaders addHeader(Header header) throws FHIRException { + public ClientHeaders addHeader(HTTPHeader header) throws FHIRException { if (headers.contains(header)) { - throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", " - + header.value + ">."); + throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", " + + header.getValue() + ">."); } headers.add(header); return this; @@ -48,39 +47,39 @@ public ClientHeaders addHeader(Header header) throws FHIRException { /** * Add a header to the list of stored headers for network operations. * - * @param headerList {@link List} of {@link Header} to add. + * @param headerList {@link List} of {@link HTTPHeader} to add. * @throws FHIRException if any of the headers being added is a duplicate. */ - public ClientHeaders addHeaders(List
headerList) throws FHIRException { + public ClientHeaders addHeaders(List headerList) throws FHIRException { headerList.forEach(this::addHeader); return this; } /** * Removes the passed in header from the list of stored headers. - * @param header {@link Header} to remove from the list. + * @param header {@link HTTPHeader} to remove from the list. * @throws FHIRException if the header passed in does not exist within the stored list. */ - public ClientHeaders removeHeader(Header header) throws FHIRException { + public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException { if (!headers.remove(header)) { - throw new FHIRException("Attempting to remove header, <" + header.name + ", " - + header.value + ">, from GenericClientHeaders that is not currently stored."); + throw new FHIRException("Attempting to remove header, <" + header.getName() + ", " + + header.getValue() + ">, from GenericClientHeaders that is not currently stored."); } return this; } /** * Removes the passed in headers from the list of stored headers. - * @param headerList {@link List} of {@link Header} to remove. + * @param headerList {@link List} of {@link HTTPHeader} to remove. * @throws FHIRException if any of the headers passed in does not exist within the stored list. */ - public ClientHeaders removeHeaders(List
headerList) throws FHIRException { + public ClientHeaders removeHeaders(List headerList) throws FHIRException { headerList.forEach(this::removeHeader); return this; } /** - * Clears all stored {@link Header}. + * Clears all stored {@link HTTPHeader}. */ public ClientHeaders clearHeaders() { headers.clear(); @@ -90,7 +89,7 @@ public ClientHeaders clearHeaders() { @Override public String toString() { return this.headers.stream() - .map(header -> "\t" + header.name + ":" + header.value) + .map(header -> "\t" + header.getName() + ":" + header.getValue()) .collect(Collectors.joining(",\n", "{\n", "\n}")); } } 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 ad15ae999a..ee0fbeba71 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 @@ 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.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 - * {@link okhttp3.Request.Builder} + * Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s to the passed in + * {@link HTTPRequest} * - * @param request {@link okhttp3.Request.Builder} to add headers to. + * @param request {@link HTTPRequest} to add headers to. * @param format Expected {@link Resource} format. - * @param headers Any additional {@link Headers} to add to the request. - */ - 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); - } - - /** - * 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())); + * @param headers Any additional {@link HTTPHeader}s to add to the request. + */ + protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable headers) { + List allHeaders = new ArrayList<>(); + request.getHeaders().forEach(allHeaders::add); + + if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add); + if (headers != null) headers.forEach(allHeaders::add); + return request.withHeaders(allHeaders); + } + 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 @@ protected static boolean hasError(OperationOutcome oo) { } /** - * 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); - /** - * 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(); + if (locationHeader != null) { + return locationHeader; } - - Authenticator proxyAuthenticator = getAuthenticator(); - - return okHttpClient.newBuilder() - .addInterceptor(new RetryInterceptor(retryCount)) - .connectTimeout(timeout, timeoutUnit) - .writeTimeout(timeout, timeoutUnit) - .readTimeout(timeout, timeoutUnit) - .proxyAuthenticator(proxyAuthenticator) - .build(); + return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER); } - @Nonnull - private static Authenticator getAuthenticator() { - return (route, response) -> { - final String httpProxyUser = System.getProperty(HTTP_PROXY_USER); - final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS); - if (httpProxyUser != null && httpProxyPass != null) { - String credential = Credentials.basic(httpProxyUser, httpProxyPass); - return response.request().newBuilder() - .header(HEADER_PROXY_AUTH, credential) - .build(); - } - return response.request().newBuilder().build(); - }; + protected ManagedFhirWebAccessor getManagedWebAccessor() { + return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger); } public FhirRequestBuilder withResourceFormat(String resourceFormat) { @@ -201,7 +123,7 @@ public FhirRequestBuilder withResourceFormat(String resourceFormat) { return this; } - public FhirRequestBuilder withHeaders(Headers headers) { + public FhirRequestBuilder withHeaders(Iterable headers) { this.headers = headers; return this; } @@ -227,25 +149,16 @@ public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) { 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 = getManagedWebAccessor().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 = getManagedWebAccessor().httpCall(requestWithHeaders); return unmarshalFeed(response, resourceFormat); } @@ -253,14 +166,14 @@ public Bundle executeAsBatch() throws IOException { * 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 @@ protected T unmarshalReference(Response response, String fo /** * Unmarshalls Bundle from response stream. */ - protected Bundle unmarshalFeed(Response response, String format) { + protected Bundle unmarshalFeed(HTTPResult response, String format) { Bundle feed = null; OperationOutcome error = null; try { - byte[] body = response.body().bytes(); - log(response.code(), response.headers(), body); - String contentType = response.header("Content-Type"); + byte[] body = response.getContent(); + + String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type"); if (body != null) { if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains(ResourceFormat.RESOURCE_JSON.getHeader()) || contentType.contains("text/xml+fhir")) { Resource rf = getParser(format).parse(body); @@ -315,7 +228,7 @@ else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { /** * Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is * provided...because reasons. - *

    + *

    * Currently supports only "json" and "xml" formats. * * @param format One of "json" or "xml". @@ -334,52 +247,4 @@ protected IParser getParser(String format) { 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/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 0000000000..54f3637986 --- /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 0000000000..18f1bed482 --- /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 7cd2b5eddd..b5789c50bb 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,107 +1,126 @@ package org.hl7.fhir.dstu3.utils.client.network; -import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; -import org.hl7.fhir.dstu3.formats.IParser; -import org.hl7.fhir.utilities.ToolingClientLogger; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.utilities.http.*; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.AdditionalMatchers; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import okhttp3.Call; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - @ExtendWith(MockitoExtension.class) public class FhirRequestBuilderTests { - private static final String DUMMY_URL = "https://some-url.com/"; - - Request mockRequest = new Request.Builder() - .url(DUMMY_URL) - .build(); - - final String RESPONSE_BODY_STRING = "{}"; + @Test + @DisplayName("Test resource format headers are added correctly (GET).") + void addResourceFormatHeadersGET() { + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET); - 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(); + Iterable headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat); - final Request.Builder requestBuilder = new Request.Builder() - .url(DUMMY_URL); + 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 + "."); - final FhirRequestBuilder fhirRequestBuilder = Mockito.spy(new FhirRequestBuilder(requestBuilder, "http://local/local")); + Assertions.assertNull(headersMap.get("Content-Type"), "Content-Type header not null."); + } - @Mock - OkHttpClient client; + @Test + @DisplayName("Test resource format headers are added correctly (POST).") + void addResourceFormatHeadersPOST() { + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST); - @Mock - Call mockCall; + Iterable headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat); - @Mock - ToolingClientLogger logger; + 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 + "."); - @BeforeEach - public void beforeEach() { - Mockito.doReturn(client).when(fhirRequestBuilder).getHttpClient(); - fhirRequestBuilder.withLogger(logger); + 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 + "\"."); } - @Nested - class RequestLoggingTests { + @Test + @DisplayName("Test that FATAL issue severity triggers error.") + void hasErrorTestFatal() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + outcome.addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.FATAL)); + Assertions.assertTrue(FhirRequestBuilder.hasError(outcome), "Error check not triggered for FATAL issue severity."); + } - @BeforeEach - public void beforeEach() throws IOException { - Mockito.doReturn(response).when(mockCall).execute(); - Mockito.doReturn(mockCall).when(client).newCall(ArgumentMatchers.any()); + @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."); + } - Mockito.doReturn(null).when(fhirRequestBuilder).unmarshalReference(ArgumentMatchers.any(), ArgumentMatchers.isNull()); - } + @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 - public void testExecuteLogging() throws IOException { - fhirRequestBuilder.execute(); - Mockito.verify(logger).logRequest(ArgumentMatchers.eq("GET"), ArgumentMatchers.eq(DUMMY_URL), ArgumentMatchers.anyList(), ArgumentMatchers.isNull()); - } + @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 - public void testExecuteBatchLogging() throws IOException { - fhirRequestBuilder.executeAsBatch(); - Mockito.verify(logger).logRequest(ArgumentMatchers.eq("GET"), ArgumentMatchers.eq(DUMMY_URL), ArgumentMatchers.anyList(), ArgumentMatchers.isNull()); - } + @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 - public void testUnmarshallReferenceLogging() { - IParser parser = Mockito.mock(IParser.class); - Mockito.doReturn(parser).when(fhirRequestBuilder).getParser(ArgumentMatchers.eq("json")); + @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"; - fhirRequestBuilder.unmarshalReference(response, "json"); - Mockito.verify(logger).logResponse(ArgumentMatchers.eq("200"), ArgumentMatchers.anyList(), AdditionalMatchers.aryEq(RESPONSE_BODY_STRING.getBytes()), ArgumentMatchers.anyLong()); + Iterable headers = List.of( + new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader), + new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) + ); + + Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); } @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 getLocationHeader returns null when no location available.") + void getLocationHeaderWhenNoLocationSet() { + Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList())); } + } diff --git a/org.hl7.fhir.r4/pom.xml b/org.hl7.fhir.r4/pom.xml index ead565cbbc..179e00ac64 100644 --- a/org.hl7.fhir.r4/pom.xml +++ b/org.hl7.fhir.r4/pom.xml @@ -35,6 +35,12 @@ + + org.projectlombok + lombok + provided + + org.fhir @@ -78,7 +84,12 @@ okio-jvm true - + + com.squareup.okhttp3 + mockwebserver + true + test + org.hl7.fhir.testcases fhir-test-cases diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java index b07046dbea..11451ad421 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java @@ -3,11 +3,10 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import lombok.Getter; +import lombok.Setter; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CapabilityStatement; @@ -29,8 +28,7 @@ import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.Utilities; -import okhttp3.Headers; -import okhttp3.internal.http2.Header; +import org.hl7.fhir.utilities.http.HTTPHeader; /** * Very Simple RESTful client. This is purely for use in the standalone tools @@ -70,16 +68,22 @@ public class FHIRToolingClient extends FHIRBaseToolingClient { private String base; private ResourceAddress resourceAddress; + @Setter private ResourceFormat preferredResourceFormat; private int maxResultSetSize = -1;// _count private CapabilityStatement capabilities; + @Getter + @Setter private Client client = new Client(); - private ArrayList

    headers = new ArrayList<>(); - private String username; - private String password; + private List headers = new ArrayList<>(); + + @Getter @Setter private String userAgent; - private String acceptLang; - private String contentLang; + @Setter + private String acceptLanguage; + + @Setter + private String contentLanguage; private int useCount; // Pass endpoint for client - URI @@ -96,14 +100,6 @@ public void initialize(String baseServiceUrl) throws URISyntaxException { this.maxResultSetSize = -1; } - public Client getClient() { - return client; - } - - public void setClient(Client client) { - this.client = client; - } - private void checkCapabilities() { try { capabilities = getCapabilitiesStatementQuick(); @@ -115,10 +111,6 @@ public String getPreferredResourceFormat() { return preferredResourceFormat.getHeader(); } - public void setPreferredResourceFormat(ResourceFormat resourceFormat) { - preferredResourceFormat = resourceFormat; - } - public int getMaximumRecordCount() { return maxResultSetSize; } @@ -131,7 +123,7 @@ public TerminologyCapabilities getTerminologyCapabilities() { TerminologyCapabilities capabilities = null; try { capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "TerminologyCapabilities", timeoutNormal).getReference(); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "TerminologyCapabilities", timeoutNormal).getReference(); } catch (Exception e) { throw new FHIRException("Error fetching the server's terminology capabilities", e); } @@ -142,7 +134,7 @@ public CapabilityStatement getCapabilitiesStatement() { CapabilityStatement conformance = null; try { conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CapabilitiesStatement", timeoutNormal).getReference(); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CapabilitiesStatement", timeoutNormal).getReference(); } catch (Exception e) { throw new FHIRException("Error fetching the server's conformance statement", e); } @@ -154,7 +146,7 @@ public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientExc return capabilities; try { capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CapabilitiesStatement-Quick", timeoutNormal) + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CapabilitiesStatement-Quick", timeoutNormal) .getReference(); } catch (Exception e) { throw new FHIRException("Error fetching the server's capability statement: " + e.getMessage(), e); @@ -167,7 +159,7 @@ public Resource read(String resourceClass, String id) {// TODO Change this to Ad ResourceRequest result = null; try { result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Read " + resourceClass + "/" + id, + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "Read " + resourceClass + "/" + id, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -184,7 +176,7 @@ public T read(Class resourceClass, String id) {// TODO C ResourceRequest result = null; try { result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Read " + resourceClass.getName() + "/" + id, + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "Read " + resourceClass.getName() + "/" + id, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -202,7 +194,7 @@ public T vread(Class resourceClass, String id, String ve try { result = client.issueGetResourceRequest( resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -220,7 +212,7 @@ public T getCanonical(Class resourceClass, String canoni try { result = client.issueGetResourceRequest( resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Read " + resourceClass.getName() + "?url=" + canonicalURL, + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "Read " + resourceClass.getName() + "?url=" + canonicalURL, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -244,7 +236,7 @@ public Resource update(Resource resource) { result = client.issuePutRequest( resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Update " + resource.fhirType() + "/" + resource.getId(), + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Update " + resource.fhirType() + "/" + resource.getId(), timeoutOperation); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -274,7 +266,7 @@ public T update(Class resourceClass, T resource, String try { result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Update " + resource.fhirType() + "/" + id, + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Update " + resource.fhirType() + "/" + id, timeoutOperation); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -312,10 +304,10 @@ public Parameters operateType(Class resourceClass, Strin URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps); if (complex) { byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true); - result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), + result = client.issuePostRequest(url, body, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "POST " + resourceClass.getName() + "/$" + name, timeoutLong); } else { - result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), + result = client.issueGetResourceRequest(url, withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "GET " + resourceClass.getName() + "/$" + name, timeoutLong); } if (result.isUnsuccessfulRequest()) { @@ -351,7 +343,7 @@ public OperationOutcome validate(Class resourceClass, T try { result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -364,10 +356,14 @@ public OperationOutcome validate(Class resourceClass, T } /** - * Helper method to prevent nesting of previously thrown EFhirClientExceptions + * Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of + * EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the + * cause. * - * @param e - * @throws EFhirClientException + * @param code The EFhirClientException code. + * @param message The EFhirClientException message. + * @param e The exception. + * @throws EFhirClientException representing the exception. */ protected void handleException(int code, String message, Exception e) throws EFhirClientException { if (e instanceof EFhirClientException) { @@ -378,11 +374,11 @@ protected void handleException(int code, String message, Exception e) throws EFh } /** - * Helper method to determine whether desired resource representation is Json or - * XML. + * Helper method to determine whether desired resource representation + * is Json or XML. * - * @param format - * @return + * @param format The format + * @return true if the format is JSON, false otherwise */ protected boolean isJson(String format) { boolean isJson = false; @@ -408,7 +404,7 @@ public Parameters lookupCode(Map params) { org.hl7.fhir.r4.utils.client.network.ResourceRequest result = null; try { result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CodeSystem/$lookup", timeoutNormal); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CodeSystem/$lookup", timeoutNormal); } catch (IOException e) { throw new FHIRException(e); } @@ -425,7 +421,7 @@ public Parameters lookupCode(Parameters p) { try { result = client.issuePostRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "CodeSystem/$lookup", timeoutNormal); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "CodeSystem/$lookup", timeoutNormal); } catch (IOException e) { throw new FHIRException(e); } @@ -442,7 +438,7 @@ public Parameters translate(Parameters p) { try { result = client.issuePostRequest(resourceAddress.resolveOperationUri(ConceptMap.class, "translate"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "ConceptMap/$translate", timeoutNormal); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "ConceptMap/$translate", timeoutNormal); } catch (IOException e) { throw new FHIRException(e); } @@ -463,7 +459,7 @@ public ValueSet expandValueset(ValueSet source, Parameters expParams) { try { result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), withVer(getPreferredResourceFormat(), "4.0"), - generateHeaders(), source == null ? "ValueSet/$expand" : "ValueSet/$expand?url=" + source.getUrl(), + generateHeaders(true), source == null ? "ValueSet/$expand" : "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), @@ -494,7 +490,7 @@ public ConceptMap initializeClosure(String name) { result = client.issuePostRequest( resourceAddress.resolveOperationUri(null, "closure", new HashMap()), ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "Closure?name=" + name, timeoutNormal); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Closure?name=" + name, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -515,7 +511,7 @@ public ConceptMap updateClosure(String name, Coding coding) { result = client.issuePostRequest( resourceAddress.resolveOperationUri(null, "closure", new HashMap()), ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), "UpdateClosure?name=" + name, timeoutOperation); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "UpdateClosure?name=" + name, timeoutOperation); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -526,22 +522,6 @@ public ConceptMap updateClosure(String name, Coding coding) { return result == null ? null : (ConceptMap) result.getPayload(); } - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public ToolingClientLogger getLogger() { return client.getLogger(); } @@ -558,50 +538,27 @@ public void setRetryCount(int retryCount) { client.setRetryCount(retryCount); } - public void setClientHeaders(ArrayList
    headers) { - this.headers = headers; + public void setClientHeaders(Iterable headers) { + this.headers = new ArrayList<>(); + headers.forEach(this.headers::add); } - private Headers generateHeaders() { - Headers.Builder builder = new Headers.Builder(); - // Add basic auth header if it exists - if (basicAuthHeaderExists()) { - builder.add(getAuthorizationHeader().toString()); - } + private Iterable generateHeaders(boolean hasBody) { // Add any other headers - if (this.headers != null) { - this.headers.forEach(header -> builder.add(header.toString())); - } + List headers = new ArrayList<>(this.headers); if (!Utilities.noString(userAgent)) { - builder.add("User-Agent: " + userAgent); + headers.add(new HTTPHeader("User-Agent",userAgent)); } - if (!Utilities.noString(acceptLang)) { - builder.add("Accept-Language: "+acceptLang); + if (!Utilities.noString(acceptLanguage)) { + headers.add(new HTTPHeader("Accept-Language", acceptLanguage)); } - if (!Utilities.noString(contentLang)) { - builder.add("Content-Language: "+contentLang); - } - - return builder.build(); - } - - public boolean basicAuthHeaderExists() { - return (username != null) && (password != null); - } - - public Header getAuthorizationHeader() { - String usernamePassword = username + ":" + password; - String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes()); - return new Header("Authorization", "Basic " + base64usernamePassword); - } - public String getUserAgent() { - return userAgent; - } + if (hasBody && !Utilities.noString(contentLanguage)) { + headers.add(new HTTPHeader("Content-Language",contentLanguage)); + } - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; + return headers; } public String getServerVersion() { @@ -609,14 +566,6 @@ public String getServerVersion() { return capabilities == null ? null : capabilities.getSoftware().getVersion(); } - public void setAcceptLanguage(String lang) { - this.acceptLang = lang; - } - - public void setContentLanguage(String lang) { - this.acceptLang = lang; - } - public Bundle search(String type, String criteria) { recordUse(); return fetchFeed(Utilities.pathURL(base, type+criteria)); @@ -627,7 +576,7 @@ public T fetchResource(Class resourceClass, String id) { org.hl7.fhir.r4.utils.client.network.ResourceRequest result = null; try { result = client.issueGetResourceRequest(resourceAddress.resolveGetResource(resourceClass, id), - withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(), resourceClass.getName()+"/"+id, timeoutNormal); + withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), resourceClass.getName()+"/"+id, timeoutNormal); } catch (IOException e) { throw new FHIRException(e); } diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/Client.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/Client.java index 37918812b9..a17a4f8d79 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/Client.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/Client.java @@ -2,148 +2,149 @@ import java.io.IOException; import java.net.URI; +import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; +import lombok.Getter; +import lombok.Setter; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.utils.client.EFhirClientException; import org.hl7.fhir.utilities.ToolingClientLogger; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPRequest; public class Client { - - public static final String DEFAULT_CHARSET = "UTF-8"; - private ToolingClientLogger logger; - private FhirLoggingInterceptor fhirLoggingInterceptor; - private int retryCount; - private String base; - - public String getBase() { - return base; - } - - public void setBase(String base) { - this.base = base; - } + @Getter @Setter + private ToolingClientLogger logger; - public ToolingClientLogger getLogger() { - return logger; - } - - public void setLogger(ToolingClientLogger logger) { - this.logger = logger; - this.fhirLoggingInterceptor = new FhirLoggingInterceptor(logger); - } - - public int getRetryCount() { - return retryCount; - } + @Getter @Setter + private int retryCount; - public void setRetryCount(int retryCount) { - this.retryCount = retryCount; - } + @Getter @Setter + private String base; public ResourceRequest issueOptionsRequest(URI optionsUri, String resourceFormat, String message, long timeout) throws IOException { - Request.Builder request = new Request.Builder().method("OPTIONS", null).url(optionsUri.toURL()); - return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(optionsUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.OPTIONS); + return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout); } public ResourceRequest issueGetResourceRequest(URI resourceUri, String resourceFormat, - Headers headers, String message, long timeout) throws IOException { - Request.Builder request = new Request.Builder().url(resourceUri.toURL()); - + Iterable headers, String message, long timeout) throws IOException { + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } - public int tester(int trytry) { - return 5; - } - public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, long timeout) throws IOException { - return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout); + return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout); } public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, - Headers headers, String message, long timeout) throws IOException { + Iterable headers, String message, long timeout) throws IOException { if (payload == null) throw new EFhirClientException("PUT requests require a non-null payload"); - RequestBody body = RequestBody.create(payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).put(body); + + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.PUT) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, long timeout) throws IOException { - return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout); + return issuePostRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout); } public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, - String resourceFormat, Headers headers, String message, long timeout) throws IOException { + String resourceFormat, Iterable headers, String message, long timeout) throws IOException { if (payload == null) throw new EFhirClientException("POST requests require a non-null payload"); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body); + + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } public boolean issueDeleteRequest(URI resourceUri, int timeout) throws IOException { - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).delete(); - return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout) + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.DELETE); + return executeFhirRequest(request, null, Collections.emptyList(), null, retryCount, timeout) .isSuccessfulRequest(); } public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat, int timeout) throws IOException { - Request.Builder request = new Request.Builder().url(resourceUri.toURL()); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle issuePostFeedRequest(URI resourceUri, Map parameters, String resourceName, Resource resource, String resourceFormat, int timeout) throws IOException { String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; byte[] payload = ByteUtils.encodeFormSubmission(parameters, resourceName, resource, boundary); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); + + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) throws IOException { if (payload == null) throw new EFhirClientException("POST requests require a non-null payload"); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); + + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout); + } + + private static String getContentTypeWithDefaultCharset(String resourceFormat) { + return resourceFormat + ";charset=" + DEFAULT_CHARSET; } - public Bundle executeBundleRequest(Request.Builder request, String resourceFormat, - Headers headers, String message, int retryCount, long timeout) throws IOException { - return new FhirRequestBuilder(request, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat) + public Bundle executeBundleRequest(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).executeAsBatch(); } - public ResourceRequest executeFhirRequest(Request.Builder request, String resourceFormat, - Headers headers, String message, int retryCount, long timeout) throws IOException { - return new FhirRequestBuilder(request, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat) + public 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.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/ClientHeaders.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/ClientHeaders.java index 6a1a6ad6aa..ef42c73b49 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/ClientHeaders.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/ClientHeaders.java @@ -6,7 +6,7 @@ import org.hl7.fhir.exceptions.FHIRException; -import okhttp3.internal.http2.Header; +import org.hl7.fhir.utilities.http.HTTPHeader; /** * Generic Implementation of Client Headers. @@ -16,29 +16,29 @@ */ public class ClientHeaders { - private final ArrayList
    headers; + private final ArrayList headers; public ClientHeaders() { this.headers = new ArrayList<>(); } - public ClientHeaders(ArrayList
    headers) { + public ClientHeaders(ArrayList headers) { this.headers = headers; } - public ArrayList
    headers() { + public ArrayList headers() { return headers; } /** * Add a header to the list of stored headers for network operations. * - * @param header {@link Header} to add to the list. + * @param header {@link HTTPHeader} to add to the list. * @throws FHIRException if the header being added is a duplicate. */ - public ClientHeaders addHeader(Header header) throws FHIRException { + public ClientHeaders addHeader(HTTPHeader header) throws FHIRException { if (headers.contains(header)) { - throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", " + header.value + ">."); + throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", " + header.getValue() + ">."); } headers.add(header); return this; @@ -47,10 +47,10 @@ public ClientHeaders addHeader(Header header) throws FHIRException { /** * Add a header to the list of stored headers for network operations. * - * @param headerList {@link List} of {@link Header} to add. + * @param headerList {@link List} of {@link HTTPHeader} to add. * @throws FHIRException if any of the headers being added is a duplicate. */ - public ClientHeaders addHeaders(List
    headerList) throws FHIRException { + public ClientHeaders addHeaders(List headerList) throws FHIRException { headerList.forEach(this::addHeader); return this; } @@ -58,13 +58,13 @@ public ClientHeaders addHeaders(List
    headerList) throws FHIRException { /** * Removes the passed in header from the list of stored headers. * - * @param header {@link Header} to remove from the list. + * @param header {@link HTTPHeader} to remove from the list. * @throws FHIRException if the header passed in does not exist within the * stored list. */ - public ClientHeaders removeHeader(Header header) throws FHIRException { + public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException { if (!headers.remove(header)) { - throw new FHIRException("Attempting to remove header, <" + header.name + ", " + header.value + throw new FHIRException("Attempting to remove header, <" + header.getName() + ", " + header.getValue() + ">, from GenericClientHeaders that is not currently stored."); } return this; @@ -73,17 +73,17 @@ public ClientHeaders removeHeader(Header header) throws FHIRException { /** * Removes the passed in headers from the list of stored headers. * - * @param headerList {@link List} of {@link Header} to remove. + * @param headerList {@link List} of {@link HTTPHeader} to remove. * @throws FHIRException if any of the headers passed in does not exist within * the stored list. */ - public ClientHeaders removeHeaders(List
    headerList) throws FHIRException { + public ClientHeaders removeHeaders(List headerList) throws FHIRException { headerList.forEach(this::removeHeader); return this; } /** - * Clears all stored {@link Header}. + * Clears all stored {@link HTTPHeader}. */ public ClientHeaders clearHeaders() { headers.clear(); @@ -92,7 +92,7 @@ public ClientHeaders clearHeaders() { @Override public String toString() { - return this.headers.stream().map(header -> "\t" + header.name + ":" + header.value) + return this.headers.stream().map(header -> "\t" + header.getName() + ":" + header.getValue()) .collect(Collectors.joining(",\n", "{\n", "\n}")); } } diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirLoggingInterceptor.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirLoggingInterceptor.java deleted file mode 100644 index 38d1953805..0000000000 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirLoggingInterceptor.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.hl7.fhir.r4.utils.client.network; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.annotation.Nonnull; - -import org.hl7.fhir.utilities.ToolingClientLogger; - -import okhttp3.Interceptor; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.Buffer; - -public class FhirLoggingInterceptor implements Interceptor { - - private ToolingClientLogger logger; - - public FhirLoggingInterceptor(ToolingClientLogger logger) { - this.logger = logger; - } - - public FhirLoggingInterceptor setLogger(ToolingClientLogger logger) { - this.logger = logger; - return this; - } - - @Override - public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException { - // Log Request - Request request = chain.request(); - List hdrs = new ArrayList<>(); - for (String s : request.headers().toString().split("\\n")) { - hdrs.add(s.trim()); - } - byte[] cnt = null; - if (request.body() != null) { - Buffer buf = new Buffer(); - request.body().writeTo(buf); - cnt = buf.readByteArray(); - } - if (logger != null) { - logger.logRequest(request.method(), request.url().toString(), hdrs, cnt); - } - - // Log Response - Response response = null; - response = chain.proceed(chain.request()); - - MediaType contentType = null; - byte[] bodyBytes = null; - if (response.body() != null) { - contentType = response.body().contentType(); - bodyBytes = response.body().bytes(); - } - - // Get Headers as List - List headerList = new ArrayList<>(); - Map> headerMap = response.headers().toMultimap(); - headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value))); - - if (logger != null) { - logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, 0); - } - - // Reading byte[] clears body. Need to recreate. - ResponseBody body = ResponseBody.create(bodyBytes, contentType); - return response.newBuilder().body(body).build(); - } -} diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilder.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilder.java index a9b0d78adb..579e3d1b30 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilder.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilder.java @@ -1,16 +1,13 @@ package org.hl7.fhir.r4.utils.client.network; -import static org.hl7.fhir.r4.utils.OperationOutcomeUtilities.outcomeFromTextError; - import java.io.IOException; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; - +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.formats.IParser; import org.hl7.fhir.r4.formats.JsonParser; import org.hl7.fhir.r4.formats.XmlParser; @@ -22,31 +19,19 @@ import org.hl7.fhir.r4.utils.client.EFhirClientException; import org.hl7.fhir.r4.utils.client.ResourceFormat; import org.hl7.fhir.utilities.MimeType; -import org.hl7.fhir.utilities.settings.FhirSettings; +import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.*; import org.hl7.fhir.utilities.xhtml.XhtmlUtils; -import okhttp3.Authenticator; -import okhttp3.Credentials; -import okhttp3.Headers; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - public class FhirRequestBuilder { - protected static final String HTTP_PROXY_USER = "http.proxyUser"; - protected static final String HTTP_PROXY_PASS = "http.proxyPassword"; - protected static final String HEADER_PROXY_AUTH = "Proxy-Authorization"; protected static final String LOCATION_HEADER = "location"; protected static final String CONTENT_LOCATION_HEADER = "content-location"; protected static final String DEFAULT_CHARSET = "UTF-8"; - /** - * The singleton instance of the HttpClient, used for all requests. - */ - private static OkHttpClient okHttpClient; - private final Request.Builder httpRequest; + + private final HTTPRequest httpRequest; private String resourceFormat = null; - private Headers headers = null; + private Iterable headers = null; private String message = null; private int retryCount = 1; /** @@ -60,63 +45,46 @@ public class FhirRequestBuilder { private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; /** - * {@link FhirLoggingInterceptor} for log output. + * {@link ToolingClientLogger} for log output. */ - private FhirLoggingInterceptor logger = null; + @Getter + @Setter + private ToolingClientLogger logger = null; + private String source; - public FhirRequestBuilder(Request.Builder httpRequest, String source) { - this.httpRequest = httpRequest; + public FhirRequestBuilder(HTTPRequest httpRequest, String source) { this.source = source; + this.httpRequest = httpRequest; } /** * Adds necessary default headers, formatting headers, and any passed in - * {@link Headers} to the passed in {@link okhttp3.Request.Builder} + * {@link HTTPHeader}s to the passed in {@link HTTPRequest} * - * @param request {@link okhttp3.Request.Builder} to add headers to. + * @param request {@link HTTPRequest} to add headers to. * @param format Expected {@link Resource} format. - * @param headers Any additional {@link Headers} to add to the request. + * @param headers Any additional {@link HTTPHeader}s to add to the request. */ - protected static void formatHeaders(Request.Builder request, String format, Headers headers) { - addDefaultHeaders(request, headers); - if (format != null) - addResourceFormatHeaders(request, format); - if (headers != null) - addHeaders(request, headers); - } + protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable 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); + if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add); + if (headers != null) headers.forEach(allHeaders::add); + return request.withHeaders(allHeaders); } - /** - * Iterates through the passed in {@link Headers} and adds them to the provided - * {@link Request.Builder}. - * - * @param request {@link Request.Builder} to add headers to. - * @param headers {@link Headers} to add to request. - */ - protected static void addHeaders(Request.Builder request, Headers headers) { - headers.forEach(header -> request.addHeader(header.getFirst(), header.getSecond())); + 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; } /** @@ -136,73 +104,24 @@ protected static boolean hasError(OperationOutcome oo) { } /** - * Extracts the 'location' header from the passes in {@link Headers}. If no + * 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; - } - } - - /** - * 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"); - } + protected static String getLocationHeader(Iterable headers) { + String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER); - if (okHttpClient == null) { - okHttpClient = new OkHttpClient(); + if (locationHeader != null) { + return locationHeader; } - - Authenticator proxyAuthenticator = getAuthenticator(); - - OkHttpClient.Builder builder = okHttpClient.newBuilder(); - if (logger != null) - builder.addInterceptor(logger); - builder.addInterceptor(new RetryInterceptor(retryCount)); - - return builder.connectTimeout(timeout, timeoutUnit).addInterceptor(new RetryInterceptor(retryCount)) - .connectTimeout(timeout, timeoutUnit).writeTimeout(timeout, timeoutUnit).readTimeout(timeout, timeoutUnit) - .proxyAuthenticator(proxyAuthenticator).build(); + return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER); } - @Nonnull - private static Authenticator getAuthenticator() { - return (route, response) -> { - final String httpProxyUser = System.getProperty(HTTP_PROXY_USER); - final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS); - if (httpProxyUser != null && httpProxyPass != null) { - String credential = Credentials.basic(httpProxyUser, httpProxyPass); - return response.request().newBuilder().header(HEADER_PROXY_AUTH, credential).build(); - } - return response.request().newBuilder().build(); - }; + protected ManagedFhirWebAccessor getManagedWebAccessor() { + return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger); } public FhirRequestBuilder withResourceFormat(String resourceFormat) { @@ -210,7 +129,7 @@ public FhirRequestBuilder withResourceFormat(String resourceFormat) { return this; } - public FhirRequestBuilder withHeaders(Headers headers) { + public FhirRequestBuilder withHeaders(Iterable headers) { this.headers = headers; return this; } @@ -225,7 +144,7 @@ public FhirRequestBuilder withRetryCount(int retryCount) { return this; } - public FhirRequestBuilder withLogger(FhirLoggingInterceptor logger) { + public FhirRequestBuilder withLogger(ToolingClientLogger logger) { this.logger = logger; return this; } @@ -236,33 +155,31 @@ public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) { return this; } - protected Request buildRequest() { - return httpRequest.build(); - } public ResourceRequest execute() throws IOException { - formatHeaders(httpRequest, resourceFormat, headers); - Response response = getHttpClient().newCall(httpRequest.build()).execute(); + HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers); + HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders); + T resource = unmarshalReference(response, resourceFormat, null); - return new ResourceRequest(resource, response.code(), getLocationHeader(response.headers())); + return new ResourceRequest(resource, response.getCode(), getLocationHeader(response.getHeaders())); } public Bundle executeAsBatch() throws IOException { - formatHeaders(httpRequest, resourceFormat, null); - Response response = getHttpClient().newCall(httpRequest.build()).execute(); - return unmarshalFeed(response, resourceFormat); + HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null); + HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders); + return unmarshalFeed(response, resourceFormat); } /** * Unmarshalls a resource from the response stream. */ @SuppressWarnings("unchecked") - protected T unmarshalReference(Response response, String format, String resourceType) { - int code = response.code(); + protected T unmarshalReference(HTTPResult response, String format, String resourceType) { + int code = response.getCode(); boolean ok = code >= 200 && code < 300; - if (response.body() == null) { + if (response.getContent() == null) { if (!ok) { - throw new EFhirClientException(response.message()); + throw new EFhirClientException(response.getMessage()); } else { return null; } @@ -271,9 +188,9 @@ protected T unmarshalReference(Response response, String fo Resource resource = null; try { - body = response.body().string(); - String ct = response.header("Content-Type"); - if (ct == null) { + body = response.getContentAsString(); + String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type"); + if (contentType == null) { if (ok) { resource = getParser(format).parse(body); } else { @@ -282,10 +199,10 @@ protected T unmarshalReference(Response response, String fo resource = OperationOutcomeUtilities.outcomeFromTextError(body); } } else { - if (ct.contains(";")) { - ct = ct.substring(0, ct.indexOf(";")); + if (contentType.contains(";")) { + contentType = contentType.substring(0, contentType.indexOf(";")); } - switch (ct) { + switch (contentType) { case "application/json": case "application/fhir+json": if (!format.contains("json")) { @@ -299,16 +216,16 @@ protected T unmarshalReference(Response response, String fo if (!format.contains("xml")) { System.out.println("Got xml response expecting "+format+" from "+source+" with status "+code); } - resource = getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(response.body().bytes()); + resource = getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(response.getContent()); break; case "text/plain": resource = OperationOutcomeUtilities.outcomeFromTextError(body); break; case "text/html" : - resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.body().string(), source)); + resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source)); break; default: // not sure what else to do? - System.out.println("Got content-type '"+ct+"' from "+source); + System.out.println("Got content-type '"+contentType+"' from "+source); System.out.println(body); resource = OperationOutcomeUtilities.outcomeFromTextError(body); } @@ -343,14 +260,14 @@ protected T unmarshalReference(Response response, String fo /** * Unmarshalls Bundle from response stream. */ - protected Bundle unmarshalFeed(Response response, String format) { + protected Bundle unmarshalFeed(HTTPResult response, String format) { return unmarshalReference(response, format, "Bundle"); } /** * Returns the appropriate parser based on the format type passed in. Defaults * to XML parser if a blank format is provided...because reasons. - *

    + *

    * Currently supports only "json" and "xml" formats. * * @param format One of "json" or "xml". diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/RetryInterceptor.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/RetryInterceptor.java deleted file mode 100644 index b90085b028..0000000000 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/network/RetryInterceptor.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.hl7.fhir.r4.utils.client.network; - -import java.io.IOException; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -/** - * An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the - * number of times we retry a to execute a given request, before reporting a - * failure. This includes unsuccessful return codes and timeouts. - */ -public class RetryInterceptor implements Interceptor { - - // Delay between retying failed requests, in millis - private final long RETRY_TIME = 2000; - - // Maximum number of times to retry the request before failing - private final int maxRetry; - - // Internal counter for tracking the number of times we've tried this request - private int retryCounter = 0; - - public RetryInterceptor(int maxRetry) { - this.maxRetry = maxRetry; - } - - @Override - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = null; - - do { - try { - // If we are retrying a failed request that failed due to a bad response from - // the server, we must close it first - if (response != null) { -// System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code()) -// + "> from url -> " + chain.request().url() + "."); - response.close(); - } - // System.out.println(chain.request().method() + " attempt <" + (retryCounter + - // 1) + "> to url -> " + chain.request().url()); - response = chain.proceed(request); - } catch (IOException e) { - try { - // Include a small break in between requests. - Thread.sleep(RETRY_TIME); - } catch (InterruptedException e1) { - System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" - + retryCounter + ">"); - } - } finally { - retryCounter++; - } - } while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1)); - - /* - * if something has gone wrong, and we are unable to complete the request, we - * still need to initialize the return response so we don't get a null pointer - * exception. - */ - return response != null ? response : chain.proceed(request); - } - -} \ No newline at end of file diff --git a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/FhirToolingClientTest.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/FhirToolingClientTest.java new file mode 100644 index 0000000000..43de724c14 --- /dev/null +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/FhirToolingClientTest.java @@ -0,0 +1,224 @@ +package org.hl7.fhir.r4.utils.client; + +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.utils.client.network.Client; +import org.hl7.fhir.r4.utils.client.network.ResourceRequest; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FhirToolingClientTest { + String TX_ADDR = "http://tx.fhir.org"; + + HTTPHeader h1 = new HTTPHeader("header1", "value1"); + HTTPHeader h2 = new HTTPHeader("header2", "value2"); + HTTPHeader h3 = new HTTPHeader("header3", "value3"); + + HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases"); + + private Client mockClient; + private FHIRToolingClient toolingClient; + + @Captor + private ArgumentCaptor> 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 TerminologyCapabilities(), 200, "location")); + Mockito + .when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) + .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); + Mockito + .when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) + .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); + + // PUT + Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); + // POST + Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); + Mockito + .when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong())) + .thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location")); + // BUNDLE REQ + Mockito + .when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong())) + .thenReturn(generateBundle()); + toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases"); + toolingClient.setClient(mockClient); + } + + private List 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.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientHeadersTest.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientHeadersTest.java new file mode 100644 index 0000000000..93ed42f405 --- /dev/null +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientHeadersTest.java @@ -0,0 +1,104 @@ +package org.hl7.fhir.r4.utils.client.network; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +public class ClientHeadersTest { + ClientHeaders clientHeaders; + + HTTPHeader h1 = new HTTPHeader("header1", "value1"); + HTTPHeader h2 = new HTTPHeader("header2", "value2"); + HTTPHeader h3 = new HTTPHeader("header3", "value3"); + + @BeforeEach + void setUp() { + clientHeaders = new ClientHeaders(); + } + + @Test + @DisplayName("Happy path add headers individually.") + void addHeader() { + clientHeaders.addHeader(h1); + Assertions.assertEquals(1, clientHeaders.headers().size()); + clientHeaders.addHeader(h2); + Assertions.assertEquals(2, clientHeaders.headers().size()); + } + + @Test + @DisplayName("Test duplicate header added individually throws FHIRException.") + void addHeaderDuplicateAdd() { + clientHeaders.addHeader(h1); + Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeader(h1)); + } + + @Test + @DisplayName("Happy path add headers as list.") + void addHeaders() { + List headersList = Arrays.asList(h1, h2, h3); + clientHeaders.addHeaders(headersList); + Assertions.assertEquals(3, clientHeaders.headers().size()); + Assertions.assertEquals(headersList, clientHeaders.headers()); + } + + @Test + @DisplayName("Happy path add headers as list.") + void addHeadersDuplicateAdd() { + List headersList = Arrays.asList(h1, h2, h1); + Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList)); + } + + @Test + @DisplayName("Happy path remove existing header.") + void removeHeader() { + clientHeaders.addHeader(h1); + clientHeaders.addHeader(h2); + clientHeaders.addHeader(h3); + clientHeaders.removeHeader(h2); + Assertions.assertEquals(2, clientHeaders.headers().size()); + clientHeaders.removeHeader(new HTTPHeader("header3", "value3")); + Assertions.assertEquals(1, clientHeaders.headers().size()); + } + + @Test + @DisplayName("Remove header not contained in list.") + void removeHeaderUnknown() { + clientHeaders.addHeader(h1); + clientHeaders.addHeader(h2); + Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeader(h3)); + } + + @Test + @DisplayName("Happy path remove list of existing headers.") + void removeHeaders() { + List headersToAddList = Arrays.asList(h1, h2, h3); + List headersToRemoveList = Arrays.asList(h2, h3); + clientHeaders.addHeaders(headersToAddList); + clientHeaders.removeHeaders(headersToRemoveList); + Assertions.assertEquals(1, clientHeaders.headers().size()); + } + + @Test + @DisplayName("Remove list containing unknown header.") + void removeHeadersUnknown() { + List headersToAddList = Arrays.asList(h1, h3); + List headersToRemoveList = Arrays.asList(h2, h3); + clientHeaders.addHeaders(headersToAddList); + Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList)); + } + + @Test + void clearHeaders() { + List headersToAddList = Arrays.asList(h1, h3); + clientHeaders.addHeaders(headersToAddList); + Assertions.assertEquals(2, clientHeaders.headers().size()); + clientHeaders.clearHeaders(); + Assertions.assertEquals(0, clientHeaders.headers().size()); + } +} diff --git a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientTest.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientTest.java new file mode 100644 index 0000000000..19cc74d1d5 --- /dev/null +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/ClientTest.java @@ -0,0 +1,140 @@ +package org.hl7.fhir.r4.utils.client.network; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.hl7.fhir.r4.context.HTMLClientLogger; +import org.hl7.fhir.r4.formats.JsonParser; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class ClientTest { + private static final long TIMEOUT = 5000; + + private MockWebServer server; + private HttpUrl serverUrl; + private Client client; + + private final Address address = new Address().setCity("Toronto").setState("Ontario").setCountry("Canada"); + private final HumanName humanName = new HumanName().addGiven("Mark").setFamily("Iantorno"); + private final Patient patient = new Patient().addName(humanName).addAddress(address) + .setGender(Enumerations.AdministrativeGender.MALE); + + @BeforeEach + void setup() { + setupMockServer(); + client = new Client(); + } + + void setupMockServer() { + server = new MockWebServer(); + serverUrl = server.url("/v1/endpoint"); + } + + byte[] generateResourceBytes(Resource resource) throws IOException { + return new JsonParser().composeBytes(resource); + } + + @Test + @DisplayName("GET request, happy path.") + void test_get_happy_path() throws IOException, URISyntaxException { + server.enqueue(new MockResponse().setBody(new String(generateResourceBytes(patient)))); + ResourceRequest 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))); + HTMLClientLogger mockLogger = Mockito.mock(HTMLClientLogger.class); + client.setLogger(mockLogger); + client.issuePostRequest(new URI(serverUrl.toString()), payload, "xml", null, TIMEOUT); + server.takeRequest(); + Mockito.verify(mockLogger, Mockito.times(1)).logRequest(Mockito.anyString(), Mockito.anyString(), Mockito.anyList(), + Mockito.any()); + Mockito.verify(mockLogger, Mockito.times(1)).logResponse(Mockito.anyString(), Mockito.anyList(), Mockito.any(), Mockito.anyLong()); + } +} diff --git a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilderTest.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilderTest.java new file mode 100644 index 0000000000..5d278d574e --- /dev/null +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/utils/client/network/FhirRequestBuilderTest.java @@ -0,0 +1,128 @@ +package org.hl7.fhir.r4.utils.client.network; + +import okhttp3.Request; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPHeaderUtil; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class FhirRequestBuilderTest { + @Test + @DisplayName("Test resource format headers are added correctly.") + void addResourceFormatHeadersGET() { + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET); + + Iterable 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.assertNull(headersMap.get("Content-Type"), "Content-Type header null."); + } + + @Test + @DisplayName("Test resource format headers are added correctly (POST).") + void addResourceFormatHeadersPOST() { + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST); + + Iterable 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 + @DisplayName("Test that FATAL issue severity triggers error.") + void hasErrorTestFatal() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.FATAL)); + Assertions.assertTrue(FhirRequestBuilder.hasError(outcome), "Error check not triggered for FATAL issue severity."); + } + + @Test + @DisplayName("Test that ERROR issue severity triggers error.") + void hasErrorTestError() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.ERROR)); + Assertions.assertTrue(FhirRequestBuilder.hasError(outcome), "Error check not triggered for ERROR issue severity."); + } + + @Test + @DisplayName("Test that no FATAL or ERROR issue severity does not trigger error.") + void hasErrorTestNoErrors() { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.INFORMATION)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.NULL)); + outcome.addIssue( + new OperationOutcome.OperationOutcomeIssueComponent().setSeverity(OperationOutcome.IssueSeverity.WARNING)); + Assertions.assertFalse(FhirRequestBuilder.hasError(outcome), "Error check triggered unexpectedly."); + } + + @Test + @DisplayName("Test that getLocationHeader returns header for 'location'.") + void getLocationHeaderWhenOnlyLocationIsSet() { + final String expectedLocationHeader = "location_header_value"; + Iterable headers = List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader)); + Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); + } + + @Test + @DisplayName("Test that getLocationHeader returns header for 'content-location'.") + void getLocationHeaderWhenOnlyContentLocationIsSet() { + final String expectedContentLocationHeader = "content_location_header_value"; + Iterable 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())); + } +} diff --git a/org.hl7.fhir.r4b/pom.xml b/org.hl7.fhir.r4b/pom.xml index 919630c724..a25a275d8e 100644 --- a/org.hl7.fhir.r4b/pom.xml +++ b/org.hl7.fhir.r4b/pom.xml @@ -34,6 +34,12 @@ + + org.projectlombok + lombok + provided + + org.apache.commons diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java index 68b94ed833..819d36fc2c 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java @@ -1,22 +1,14 @@ package org.hl7.fhir.r4b.terminologies; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Base64; import java.util.Date; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -156,9 +148,9 @@ public void commit(String token) throws IOException { String url = "https://tx.fhir.org/post/tx-cache/" + ghOrg + "/" + ghRepo + "/" + ghBranch + ".zip"; System.out.println("Sending tx-cache to " + url + " (" + Utilities.describeSize(bs.toByteArray().length) + ")"); - HTTPResult res = ManagedWebAccess.builder() + HTTPResult res = ManagedWebAccess.accessor() .withBasicAuth(token.substring(0, token.indexOf(':')), token.substring(token.indexOf(':') + 1)) - .withAccept("application/zip").put(url, bs.toByteArray(), null); + .put(url, bs.toByteArray(), null, "application/zip"); if (res.getCode() >= 300) { System.out.println("sending cache failed: " + res.getCode()); } else { diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClient.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClient.java index 65f74dc192..5587639327 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClient.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClient.java @@ -1,7 +1,8 @@ package org.hl7.fhir.r4b.utils.client; -import okhttp3.Headers; -import okhttp3.internal.http2.Header; +import lombok.Getter; +import lombok.Setter; + import org.hl7.fhir.exceptions.FHIRException; /* @@ -37,14 +38,13 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r4b.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4b.utils.client.network.ByteUtils; import org.hl7.fhir.r4b.utils.client.network.Client; -import org.hl7.fhir.r4b.utils.client.network.ClientHeaders; import org.hl7.fhir.r4b.utils.client.network.ResourceRequest; import org.hl7.fhir.utilities.FHIRBaseToolingClient; import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.http.HTTPHeader; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.*; @@ -90,12 +90,21 @@ public class FHIRToolingClient extends FHIRBaseToolingClient{ private ResourceFormat preferredResourceFormat; private int maxResultSetSize = -1;// _count private CapabilityStatement capabilities; + @Getter + @Setter private Client client = new Client(); - private ArrayList

    headers = new ArrayList<>(); - private String username; - private String password; + private List headers = new ArrayList<>(); + + @Setter + @Getter private String userAgent; + @Setter + private String acceptLanguage; + + @Setter + private String contentLanguage; + // Pass endpoint for client - URI public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException { preferredResourceFormat = ResourceFormat.RESOURCE_XML; @@ -111,14 +120,6 @@ public void initialize(String baseServiceUrl) throws URISyntaxException { checkCapabilities(); } - public Client getClient() { - return client; - } - - public void setClient(Client client) { - this.client = client; - } - private void checkCapabilities() { try { capabilities = getCapabilitiesStatementQuick(); @@ -146,7 +147,7 @@ public TerminologyCapabilities getTerminologyCapabilities() { TerminologyCapabilities capabilities = null; try { capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), - getPreferredResourceFormat(), generateHeaders(), "TerminologyCapabilities", timeoutNormal).getReference(); + getPreferredResourceFormat(), generateHeaders(false), "TerminologyCapabilities", timeoutNormal).getReference(); } catch (Exception e) { throw new FHIRException("Error fetching the server's terminology capabilities", e); } @@ -157,7 +158,7 @@ public CapabilityStatement getCapabilitiesStatement() { CapabilityStatement conformance = null; try { conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false), - getPreferredResourceFormat(), generateHeaders(), "CapabilitiesStatement", timeoutNormal).getReference(); + getPreferredResourceFormat(), generateHeaders(false), "CapabilitiesStatement", timeoutNormal).getReference(); } catch (Exception e) { throw new FHIRException("Error fetching the server's conformance statement", e); } @@ -169,7 +170,7 @@ public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientExc return capabilities; try { capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true), - getPreferredResourceFormat(), generateHeaders(), "CapabilitiesStatement-Quick", timeoutNormal) + getPreferredResourceFormat(), generateHeaders(false), "CapabilitiesStatement-Quick", timeoutNormal) .getReference(); } catch (Exception e) { throw new FHIRException("Error fetching the server's capability statement: " + e.getMessage(), e); @@ -181,7 +182,7 @@ public T read(Class resourceClass, String id) {// TODO C ResourceRequest result = null; try { result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), - getPreferredResourceFormat(), generateHeaders(), "Read " + resourceClass.getName() + "/" + id, + getPreferredResourceFormat(), generateHeaders(false), "Read " + resourceClass.getName() + "/" + id, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), @@ -198,7 +199,7 @@ public T vread(Class resourceClass, String id, String ve try { result = client.issueGetResourceRequest( resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version), - getPreferredResourceFormat(), generateHeaders(), + getPreferredResourceFormat(), generateHeaders(false), "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), @@ -215,7 +216,7 @@ public T getCanonical(Class resourceClass, String canoni try { result = client.issueGetResourceRequest( resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL), - getPreferredResourceFormat(), generateHeaders(), "Read " + resourceClass.getName() + "?url=" + canonicalURL, + getPreferredResourceFormat(), generateHeaders(false), "Read " + resourceClass.getName() + "?url=" + canonicalURL, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), @@ -238,7 +239,7 @@ public Resource update(Resource resource) { result = client.issuePutRequest( resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - getPreferredResourceFormat(), generateHeaders(), "Update " + resource.fhirType() + "/" + resource.getId(), + getPreferredResourceFormat(), generateHeaders(true), "Update " + resource.fhirType() + "/" + resource.getId(), timeoutOperation); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), @@ -267,7 +268,7 @@ public T update(Class resourceClass, T resource, String try { result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - getPreferredResourceFormat(), generateHeaders(), "Update " + resource.fhirType() + "/" + id, + getPreferredResourceFormat(), generateHeaders(true), "Update " + resource.fhirType() + "/" + id, timeoutOperation); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), @@ -305,10 +306,10 @@ public Parameters operateType(Class resourceClass, Strin URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps); if (complex) { byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true); - result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(), + result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(true), "POST " + resourceClass.getName() + "/$" + name, timeoutLong); } else { - result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), + result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(false), "GET " + resourceClass.getName() + "/$" + name, timeoutLong); } if (result.isUnsuccessfulRequest()) { @@ -334,7 +335,7 @@ public Bundle transaction(Bundle batch) { try { transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat()), false), - getPreferredResourceFormat(), generateHeaders(), "transaction", + getPreferredResourceFormat(), generateHeaders(true), "transaction", timeoutOperation + (timeoutEntry * batch.getEntry().size())); } catch (Exception e) { handleException("An error occurred trying to process this transaction request", e); @@ -348,7 +349,7 @@ public OperationOutcome validate(Class resourceClass, T try { result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat()), false), - getPreferredResourceFormat(), generateHeaders(), + getPreferredResourceFormat(), generateHeaders(true), "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), @@ -361,10 +362,14 @@ public OperationOutcome validate(Class resourceClass, T } /** - * Helper method to prevent nesting of previously thrown EFhirClientExceptions + * Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of + * EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the + * cause. * - * @param e - * @throws EFhirClientException + + * @param message The EFhirClientException message. + * @param e The exception + * @throws EFhirClientException EFhirClientException representing the exception. */ protected void handleException(String message, Exception e) throws EFhirClientException { if (e instanceof EFhirClientException) { @@ -378,8 +383,8 @@ protected void handleException(String message, Exception e) throws EFhirClientEx * Helper method to determine whether desired resource representation is Json or * XML. * - * @param format - * @return + * @param format the format + * @return true if the format is JSON, false otherwise */ protected boolean isJson(String format) { boolean isJson = false; @@ -406,7 +411,7 @@ public ValueSet expandValueset(ValueSet source, Parameters expParams) { try { result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"), ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), getPreferredResourceFormat(), - generateHeaders(), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand); + generateHeaders(true), "ValueSet/$expand?url=" + source.getUrl(), timeoutExpand); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -421,7 +426,7 @@ public Parameters lookupCode(Map params) { org.hl7.fhir.r4b.utils.client.network.ResourceRequest result = null; try { result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), - getPreferredResourceFormat(), generateHeaders(), "CodeSystem/$lookup", timeoutNormal); + getPreferredResourceFormat(), generateHeaders(false), "CodeSystem/$lookup", timeoutNormal); } catch (IOException e) { e.printStackTrace(); } @@ -443,7 +448,7 @@ public ValueSet expandValueset(ValueSet source, Parameters expParams, Map()), ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true), - getPreferredResourceFormat(), generateHeaders(), "Closure?name=" + name, timeoutNormal); + getPreferredResourceFormat(), generateHeaders(true), "Closure?name=" + name, timeoutNormal); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -486,7 +491,7 @@ public ConceptMap updateClosure(String name, Coding coding) { result = client.issuePostRequest( resourceAddress.resolveOperationUri(null, "closure", new HashMap()), ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true), - getPreferredResourceFormat(), generateHeaders(), "UpdateClosure?name=" + name, timeoutOperation); + getPreferredResourceFormat(), generateHeaders(true), "UpdateClosure?name=" + name, timeoutOperation); if (result.isUnsuccessfulRequest()) { throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); @@ -497,22 +502,6 @@ public ConceptMap updateClosure(String name, Coding coding) { return result == null ? null : (ConceptMap) result.getPayload(); } - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public long getTimeout() { return client.getTimeout(); } @@ -537,42 +526,27 @@ public void setRetryCount(int retryCount) { client.setRetryCount(retryCount); } - public void setClientHeaders(ArrayList
    headers) { - this.headers = headers; + public void setClientHeaders(Iterable headers) { + this.headers = new ArrayList<>(); + headers.forEach(this.headers::add); } - private Headers generateHeaders() { - Headers.Builder builder = new Headers.Builder(); - // Add basic auth header if it exists - if (basicAuthHeaderExists()) { - builder.add(getAuthorizationHeader().toString()); - } + private Iterable generateHeaders(boolean hasBody) { // Add any other headers - if (this.headers != null) { - this.headers.forEach(header -> builder.add(header.toString())); - } + List headers = new ArrayList<>(this.headers); if (!Utilities.noString(userAgent)) { - builder.add("User-Agent: " + userAgent); + headers.add(new HTTPHeader("User-Agent",userAgent)); } - return builder.build(); - } - public boolean basicAuthHeaderExists() { - return (username != null) && (password != null); - } - - public Header getAuthorizationHeader() { - String usernamePassword = username + ":" + password; - String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes()); - return new Header("Authorization", "Basic " + base64usernamePassword); - } + if (!Utilities.noString(acceptLanguage)) { + headers.add(new HTTPHeader("Accept-Language", acceptLanguage)); + } - public String getUserAgent() { - return userAgent; - } + if (hasBody && !Utilities.noString(contentLanguage)) { + headers.add(new HTTPHeader("Content-Language",contentLanguage)); + } - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; + return headers; } public String getServerVersion() { diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/Client.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/Client.java index 6cf5fa10f9..d5f145f07e 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/Client.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/Client.java @@ -1,16 +1,17 @@ package org.hl7.fhir.r4b.utils.client.network; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; +import lombok.Getter; +import lombok.Setter; import org.hl7.fhir.r4b.model.Bundle; import org.hl7.fhir.r4b.model.Resource; import org.hl7.fhir.r4b.utils.client.EFhirClientException; import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.hl7.fhir.utilities.http.HTTPHeader; import java.io.IOException; import java.net.URI; +import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -18,144 +19,132 @@ public class Client { public static final String DEFAULT_CHARSET = "UTF-8"; private static final long DEFAULT_TIMEOUT = 5000; + @Getter @Setter private ToolingClientLogger logger; - private FhirLoggingInterceptor fhirLoggingInterceptor; - private int retryCount; - private long timeout = DEFAULT_TIMEOUT; - private byte[] payload; - private String base; - - public String getBase() { - return base; - } - - public void setBase(String base) { - this.base = base; - } - - public ToolingClientLogger getLogger() { - return logger; - } - - public void setLogger(ToolingClientLogger logger) { - this.logger = logger; - this.fhirLoggingInterceptor = new FhirLoggingInterceptor(logger); - } - - public int getRetryCount() { - return retryCount; - } - - public void setRetryCount(int retryCount) { - this.retryCount = retryCount; - } + @Setter + @Getter + private int retryCount; - public long getTimeout() { - return timeout; - } + @Setter + @Getter + private long timeout = DEFAULT_TIMEOUT; - public void setTimeout(long timeout) { - this.timeout = timeout; - } + @Setter @Getter + private String base; public ResourceRequest issueOptionsRequest(URI optionsUri, String resourceFormat, String message, long timeout) throws IOException { - this.payload = null; - Request.Builder request = new Request.Builder().method("OPTIONS", null).url(optionsUri.toURL()); - - return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(optionsUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.OPTIONS); + return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout); } public ResourceRequest issueGetResourceRequest(URI resourceUri, String resourceFormat, - Headers headers, String message, long timeout) throws IOException { - this.payload = null; - Request.Builder request = new Request.Builder().url(resourceUri.toURL()); - + Iterable headers, String message, long timeout) throws IOException { + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } - public int tester(int trytry) { - return 5; - } - public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, long timeout) throws IOException { - return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout); + return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout); } public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, - Headers headers, String message, long timeout) throws IOException { + Iterable headers, String message, long timeout) throws IOException { if (payload == null) throw new EFhirClientException("PUT requests require a non-null payload"); - this.payload = payload; - RequestBody body = RequestBody.create(payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).put(body); + + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.PUT) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, long timeout) throws IOException { - return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout); + return issuePostRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout); } public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, - String resourceFormat, Headers headers, String message, long timeout) throws IOException { + String resourceFormat, Iterable headers, String message, long timeout) throws IOException { if (payload == null) throw new EFhirClientException("POST requests require a non-null payload"); - this.payload = payload; - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body); + + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } public boolean issueDeleteRequest(URI resourceUri) throws IOException { - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).delete(); - return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout) + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.DELETE); + return executeFhirRequest(request, null, Collections.emptyList(), null, retryCount, timeout) .isSuccessfulRequest(); } public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException { - Request.Builder request = new Request.Builder().url(resourceUri.toURL()); - - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle issuePostFeedRequest(URI resourceUri, Map parameters, String resourceName, Resource resource, String resourceFormat) throws IOException { String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; byte[] payload = ByteUtils.encodeFormSubmission(parameters, resourceName, resource, boundary); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } - public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, Headers headers, + public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, Iterable headers, String message, int timeout) throws IOException { if (payload == null) throw new EFhirClientException("POST requests require a non-null payload"); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder().url(resourceUri.toURL()).post(body); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeBundleRequest(request, resourceFormat, headers, message, retryCount, timeout); } - public Bundle executeBundleRequest(Request.Builder request, String resourceFormat, - Headers headers, String message, int retryCount, long timeout) throws IOException { - return new FhirRequestBuilder(request, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat) + private static String getContentTypeWithDefaultCharset(String resourceFormat) { + return resourceFormat + ";charset=" + DEFAULT_CHARSET; + } + + public Bundle executeBundleRequest(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).executeAsBatch(); } - public ResourceRequest executeFhirRequest(Request.Builder request, String resourceFormat, - Headers headers, String message, int retryCount, long timeout) throws IOException { - return new FhirRequestBuilder(request, base).withLogger(fhirLoggingInterceptor).withResourceFormat(resourceFormat) + public 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.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/ClientHeaders.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/ClientHeaders.java index 6b6999d8d1..b837bd094c 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/ClientHeaders.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/ClientHeaders.java @@ -1,7 +1,8 @@ package org.hl7.fhir.r4b.utils.client.network; -import okhttp3.internal.http2.Header; + import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.http.HTTPHeader; import java.util.ArrayList; import java.util.List; @@ -9,35 +10,35 @@ /** * Generic Implementation of Client Headers. - * + *

    * Stores a list of headers for HTTP calls to the TX server. Users can implement * their own instance if they desire specific, custom behavior. */ public class ClientHeaders { - private final ArrayList

    headers; + private final ArrayList headers; public ClientHeaders() { this.headers = new ArrayList<>(); } - public ClientHeaders(ArrayList
    headers) { + public ClientHeaders(ArrayList headers) { this.headers = headers; } - public ArrayList
    headers() { + public ArrayList headers() { return headers; } /** * Add a header to the list of stored headers for network operations. * - * @param header {@link Header} to add to the list. + * @param header {@link HTTPHeader} to add to the list. * @throws FHIRException if the header being added is a duplicate. */ - public ClientHeaders addHeader(Header header) throws FHIRException { + public ClientHeaders addHeader(HTTPHeader header) throws FHIRException { if (headers.contains(header)) { - throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", " + header.value + ">."); + throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", " + header.getValue() + ">."); } headers.add(header); return this; @@ -46,10 +47,10 @@ public ClientHeaders addHeader(Header header) throws FHIRException { /** * Add a header to the list of stored headers for network operations. * - * @param headerList {@link List} of {@link Header} to add. + * @param headerList {@link List} of {@link HTTPHeader} to add. * @throws FHIRException if any of the headers being added is a duplicate. */ - public ClientHeaders addHeaders(List
    headerList) throws FHIRException { + public ClientHeaders addHeaders(List headerList) throws FHIRException { headerList.forEach(this::addHeader); return this; } @@ -57,13 +58,13 @@ public ClientHeaders addHeaders(List
    headerList) throws FHIRException { /** * Removes the passed in header from the list of stored headers. * - * @param header {@link Header} to remove from the list. + * @param header {@link HTTPHeader} to remove from the list. * @throws FHIRException if the header passed in does not exist within the * stored list. */ - public ClientHeaders removeHeader(Header header) throws FHIRException { + public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException { if (!headers.remove(header)) { - throw new FHIRException("Attempting to remove header, <" + header.name + ", " + header.value + throw new FHIRException("Attempting to remove header, <" + header.getName() + ", " + header.getValue() + ">, from GenericClientHeaders that is not currently stored."); } return this; @@ -72,17 +73,17 @@ public ClientHeaders removeHeader(Header header) throws FHIRException { /** * Removes the passed in headers from the list of stored headers. * - * @param headerList {@link List} of {@link Header} to remove. + * @param headerList {@link List} of {@link HTTPHeader} to remove. * @throws FHIRException if any of the headers passed in does not exist within * the stored list. */ - public ClientHeaders removeHeaders(List
    headerList) throws FHIRException { + public ClientHeaders removeHeaders(List headerList) throws FHIRException { headerList.forEach(this::removeHeader); return this; } /** - * Clears all stored {@link Header}. + * Clears all stored {@link HTTPHeader}. */ public ClientHeaders clearHeaders() { headers.clear(); @@ -91,7 +92,7 @@ public ClientHeaders clearHeaders() { @Override public String toString() { - return this.headers.stream().map(header -> "\t" + header.name + ":" + header.value) + return this.headers.stream().map(header -> "\t" + header.getName() + ":" + header.getValue()) .collect(Collectors.joining(",\n", "{\n", "\n}")); } } diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirLoggingInterceptor.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirLoggingInterceptor.java deleted file mode 100644 index 7a9a6a2821..0000000000 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirLoggingInterceptor.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.hl7.fhir.r4b.utils.client.network; - -import okhttp3.*; -import okio.Buffer; - -import org.hl7.fhir.utilities.ToolingClientLogger; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class FhirLoggingInterceptor implements Interceptor { - - private ToolingClientLogger logger; - - public FhirLoggingInterceptor(ToolingClientLogger logger) { - this.logger = logger; - } - - public FhirLoggingInterceptor setLogger(ToolingClientLogger logger) { - this.logger = logger; - return this; - } - - @Override - public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException { - // Log Request - Request request = chain.request(); - List hdrs = new ArrayList<>(); - for (String s : request.headers().toString().split("\\n")) { - hdrs.add(s.trim()); - } - byte[] cnt = null; - if (request.body() != null) { - Buffer buf = new Buffer(); - request.body().writeTo(buf); - cnt = buf.readByteArray(); - } - if (logger != null) { - logger.logRequest(request.method(), request.url().toString(), hdrs, cnt); - } - - // Log Response - Response response = null; - response = chain.proceed(chain.request()); - - MediaType contentType = null; - byte[] bodyBytes = null; - if (response.body() != null) { - contentType = response.body().contentType(); - bodyBytes = response.body().bytes(); - } - - // Get Headers as List - List headerList = new ArrayList<>(); - Map> headerMap = response.headers().toMultimap(); - headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value))); - - if (logger != null) { - logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, 0); - } - - // Reading byte[] clears body. Need to recreate. - ResponseBody body = ResponseBody.create(bodyBytes, contentType); - return response.newBuilder().body(body).build(); - } -} diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilder.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilder.java index fa7a663008..d8f2536837 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilder.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilder.java @@ -1,11 +1,10 @@ package org.hl7.fhir.r4b.utils.client.network; import java.io.IOException; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4b.formats.IParser; @@ -18,15 +17,10 @@ import org.hl7.fhir.r4b.utils.ResourceUtilities; import org.hl7.fhir.r4b.utils.client.EFhirClientException; import org.hl7.fhir.r4b.utils.client.ResourceFormat; +import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.*; import org.hl7.fhir.utilities.xhtml.XhtmlUtils; -import okhttp3.Authenticator; -import okhttp3.Credentials; -import okhttp3.Headers; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - public class FhirRequestBuilder { protected static final String HTTP_PROXY_USER = "http.proxyUser"; @@ -35,13 +29,10 @@ public class FhirRequestBuilder { protected static final String LOCATION_HEADER = "location"; protected static final String CONTENT_LOCATION_HEADER = "content-location"; protected static final String DEFAULT_CHARSET = "UTF-8"; - /** - * The singleton instance of the HttpClient, used for all requests. - */ - private static OkHttpClient okHttpClient; - private final Request.Builder httpRequest; + + private final HTTPRequest httpRequest; private String resourceFormat = null; - private Headers headers = null; + private Iterable headers = null; private String message = null; private int retryCount = 1; /** @@ -55,63 +46,43 @@ public class FhirRequestBuilder { private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; /** - * {@link FhirLoggingInterceptor} for log output. + * {@link ToolingClientLogger} for log output. */ - private FhirLoggingInterceptor logger = null; + private ToolingClientLogger logger = null; private String source; - public FhirRequestBuilder(Request.Builder httpRequest, String source) { - this.httpRequest = httpRequest; + public FhirRequestBuilder(HTTPRequest httpRequest, String source) { this.source = source; + this.httpRequest = httpRequest; } /** * Adds necessary default headers, formatting headers, and any passed in - * {@link Headers} to the passed in {@link okhttp3.Request.Builder} + * {@link HTTPHeader} to the passed in {@link okhttp3.Request.Builder} * * @param request {@link okhttp3.Request.Builder} to add headers to. * @param format Expected {@link Resource} format. - * @param headers Any additional {@link Headers} to add to the request. - */ - 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); - } - - /** - * Adds necessary headers for all REST requests. - *
  • User-Agent : hapi-fhir-tooling-client
  • - * - * @param request {@link Request.Builder} to add default headers to. + * @param headers Any additional {@link HTTPHeader} to add to the request. */ - protected static void addDefaultHeaders(Request.Builder request, Headers headers) { - if (headers == null || !headers.names().contains("User-Agent")) { - request.addHeader("User-Agent", "hapi-fhir-tooling-client"); - } - } + protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable headers) { + List allHeaders = new ArrayList<>(); + request.getHeaders().forEach(allHeaders::add); - /** - * 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); + if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add); + if (headers != null) headers.forEach(allHeaders::add); + return request.withHeaders(allHeaders); } - /** - * Iterates through the passed in {@link Headers} and adds them to the provided - * {@link Request.Builder}. - * - * @param request {@link Request.Builder} to add headers to. - * @param headers {@link Headers} to add to request. - */ - protected static void addHeaders(Request.Builder request, Headers headers) { - headers.forEach(header -> request.addHeader(header.getFirst(), header.getSecond())); + 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,68 +102,24 @@ protected static boolean hasError(OperationOutcome oo) { } /** - * Extracts the 'location' header from the passes in {@link Headers}. If no + * Extracts the 'location' header from the passed in {@link HTTPHeader}s. If no * value for 'location' exists, the value for 'content-location' is returned. If * neither header exists, we return null. * - * @param headers {@link Headers} to evaluate + * @param headers {@link HTTPHeader}s to evaluate * @return {@link String} header value, or null if no location headers are set. */ - protected static String getLocationHeader(Headers headers) { - Map> 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); - /** - * 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 (okHttpClient == null) { - okHttpClient = new OkHttpClient(); + if (locationHeader != null) { + return locationHeader; } - - Authenticator proxyAuthenticator = getAuthenticator(); - - OkHttpClient.Builder builder = okHttpClient.newBuilder(); - if (logger != null) - builder.addInterceptor(logger); - builder.addInterceptor(new RetryInterceptor(retryCount)); - - return builder.connectTimeout(timeout, timeoutUnit).writeTimeout(timeout, timeoutUnit) - .readTimeout(timeout, timeoutUnit).proxyAuthenticator(proxyAuthenticator).build(); + return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER); } - @Nonnull - private static Authenticator getAuthenticator() { - return (route, response) -> { - final String httpProxyUser = System.getProperty(HTTP_PROXY_USER); - final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS); - if (httpProxyUser != null && httpProxyPass != null) { - String credential = Credentials.basic(httpProxyUser, httpProxyPass); - return response.request().newBuilder().header(HEADER_PROXY_AUTH, credential).build(); - } - return response.request().newBuilder().build(); - }; + protected ManagedFhirWebAccessor getManagedWebAccessor() { + return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger); } public FhirRequestBuilder withResourceFormat(String resourceFormat) { @@ -200,7 +127,7 @@ public FhirRequestBuilder withResourceFormat(String resourceFormat) { return this; } - public FhirRequestBuilder withHeaders(Headers headers) { + public FhirRequestBuilder withHeaders(Iterable headers) { this.headers = headers; return this; } @@ -215,7 +142,7 @@ public FhirRequestBuilder withRetryCount(int retryCount) { return this; } - public FhirRequestBuilder withLogger(FhirLoggingInterceptor logger) { + public FhirRequestBuilder withLogger(ToolingClientLogger logger) { this.logger = logger; return this; } @@ -226,33 +153,28 @@ public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) { return this; } - protected Request buildRequest() { - return httpRequest.build(); - } - public ResourceRequest execute() throws IOException { - formatHeaders(httpRequest, resourceFormat, headers); - Response response = getHttpClient().newCall(httpRequest.build()).execute(); + HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers); + HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders); T resource = unmarshalReference(response, resourceFormat, null); - return new ResourceRequest(resource, response.code(), getLocationHeader(response.headers())); + return new ResourceRequest(resource, response.getCode(), getLocationHeader(response.getHeaders())); } public Bundle executeAsBatch() throws IOException { - formatHeaders(httpRequest, resourceFormat, null); - Response response = getHttpClient().newCall(httpRequest.build()).execute(); - return unmarshalFeed(response, resourceFormat); + HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null); + HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);return unmarshalFeed(response, resourceFormat); } /** * Unmarshalls a resource from the response stream. */ @SuppressWarnings("unchecked") - protected T unmarshalReference(Response response, String format, String resourceType) { - int code = response.code(); + protected T unmarshalReference(HTTPResult response, String format, String resourceType) { + int code = response.getCode(); boolean ok = code >= 200 && code < 300; - if (response.body() == null) { + if (response.getContent() == null) { if (!ok) { - throw new EFhirClientException(response.message()); + throw new EFhirClientException(response.getMessage()); } else { return null; } @@ -261,9 +183,9 @@ protected T unmarshalReference(Response response, String fo Resource resource = null; try { - body = response.body().string(); - String ct = response.header("Content-Type"); - if (ct == null) { + body = response.getContentAsString(); + String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type"); + if (contentType == null) { if (ok) { resource = getParser(format).parse(body); } else { @@ -272,10 +194,10 @@ protected T unmarshalReference(Response response, String fo resource = OperationOutcomeUtilities.outcomeFromTextError(body); } } else { - if (ct.contains(";")) { - ct = ct.substring(0, ct.indexOf(";")); + if (contentType.contains(";")) { + contentType = contentType.substring(0, contentType.indexOf(";")); } - switch (ct) { + switch (contentType) { case "application/json": case "application/fhir+json": if (!format.contains("json")) { @@ -295,10 +217,10 @@ protected T unmarshalReference(Response response, String fo resource = OperationOutcomeUtilities.outcomeFromTextError(body); break; case "text/html" : - resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.body().string(), source)); + resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source)); break; default: // not sure what else to do? - System.out.println("Got content-type '"+ct+"' from "+source); + System.out.println("Got content-type '"+contentType+"' from "+source); System.out.println(body); resource = OperationOutcomeUtilities.outcomeFromTextError(body); } @@ -333,7 +255,7 @@ protected T unmarshalReference(Response response, String fo /** * Unmarshalls Bundle from response stream. */ - protected Bundle unmarshalFeed(Response response, String format) { + protected Bundle unmarshalFeed(HTTPResult response, String format) { return unmarshalReference(response, format, "Bundle"); } diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/RetryInterceptor.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/RetryInterceptor.java deleted file mode 100644 index 931f66ddde..0000000000 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/client/network/RetryInterceptor.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.hl7.fhir.r4b.utils.client.network; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -import java.io.IOException; - -/** - * An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the - * number of times we retry a to execute a given request, before reporting a - * failure. This includes unsuccessful return codes and timeouts. - */ -public class RetryInterceptor implements Interceptor { - - // Delay between retying failed requests, in millis - private final long RETRY_TIME = 2000; - - // Maximum number of times to retry the request before failing - private final int maxRetry; - - // Internal counter for tracking the number of times we've tried this request - private int retryCounter = 0; - - public RetryInterceptor(int maxRetry) { - this.maxRetry = maxRetry; - } - - @Override - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = null; - - do { - try { - // If we are retrying a failed request that failed due to a bad response from - // the server, we must close it first - if (response != null) { -// System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code()) -// + "> from url -> " + chain.request().url() + "."); - response.close(); - } - // System.out.println(chain.request().method() + " attempt <" + (retryCounter + - // 1) + "> to url -> " + chain.request().url()); - response = chain.proceed(request); - } catch (IOException e) { - try { - // Include a small break in between requests. - Thread.sleep(RETRY_TIME); - } catch (InterruptedException e1) { - System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" - + retryCounter + ">"); - } - } finally { - retryCounter++; - } - } while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1)); - - /* - * if something has gone wrong, and we are unable to complete the request, we - * still need to initialize the return response so we don't get a null pointer - * exception. - */ - return response != null ? response : chain.proceed(request); - } - -} \ No newline at end of file diff --git a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClientTest.java b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClientTest.java index eecbc0234e..83099c4c6a 100644 --- a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClientTest.java +++ b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/FHIRToolingClientTest.java @@ -1,79 +1,89 @@ package org.hl7.fhir.r4b.utils.client; -import okhttp3.Headers; -import okhttp3.Request; -import okhttp3.internal.http2.Header; import org.hl7.fhir.r4b.model.*; import org.hl7.fhir.r4b.utils.client.network.Client; import org.hl7.fhir.r4b.utils.client.network.ResourceRequest; -import org.junit.jupiter.api.Assertions; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.hl7.fhir.utilities.http.HTTPHeader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; +import org.mockito.*; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; class FHIRToolingClientTest { String TX_ADDR = "http://tx.fhir.org"; - Header h1 = new Header("header1", "value1"); - Header h2 = new Header("header2", "value2"); - Header h3 = new Header("header3", "value3"); + HTTPHeader h1 = new HTTPHeader("header1", "value1"); + HTTPHeader h2 = new HTTPHeader("header2", "value2"); + HTTPHeader h3 = new HTTPHeader("header3", "value3"); + + HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases"); private Client mockClient; private FHIRToolingClient toolingClient; + @Captor + private ArgumentCaptor> 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(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); Mockito .when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location")); Mockito .when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); Mockito .when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); // PUT Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); // POST Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())).thenReturn(resourceResourceRequest); Mockito .when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.contains("validate"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location")); // BUNDLE REQ Mockito - .when(mockClient.executeBundleRequest(Mockito.any(Request.Builder.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong())) + .when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong())) .thenReturn(generateBundle()); toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases"); toolingClient.setClient(mockClient); } - private ArrayList
    getHeaders() { + 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(); @@ -115,108 +125,102 @@ private Observation generateObservation() { return observation; } - private void checkHeaders(Headers argumentCaptorValue) { - getHeaders().forEach(header -> { -// System.out.println("Checking header <" + header.component1().utf8() + ", " + header.component2().utf8() + ">"); - Assertions.assertEquals(argumentCaptorValue.get(header.component1().utf8()), header.component2().utf8()); + private void checkHeaders(Iterable argumentCaptorValue) { + List capturedHeaders = new ArrayList<>(); + argumentCaptorValue.forEach(capturedHeaders::add); + + getHeadersWithAgent().forEach(header -> { + assertTrue(capturedHeaders.contains(header)); }); } @Test void getTerminologyCapabilities() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getTerminologyCapabilities(); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getCapabilitiesStatement() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCapabilitiesStatement(); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getCapabilitiesStatementQuick() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCapabilitiesStatementQuick(); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void read() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.read(Patient.class, "id"); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void vread() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.vread(Patient.class, "id", "version"); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getCanonical() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCanonical(Patient.class, "canonicalURL"); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void update() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.update(generatePatient()); Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void validate() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.validate(Patient.class, generatePatient(), "id"); Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } } \ No newline at end of file diff --git a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/ClientHeadersTest.java b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/ClientHeadersTest.java index 75c7129d92..72051706ce 100644 --- a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/ClientHeadersTest.java +++ b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/ClientHeadersTest.java @@ -1,7 +1,7 @@ package org.hl7.fhir.r4b.utils.client.network; -import okhttp3.internal.http2.Header; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.http.HTTPHeader; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,9 +14,9 @@ class ClientHeadersTest { ClientHeaders clientHeaders; - Header h1 = new Header("header1", "value1"); - Header h2 = new Header("header2", "value2"); - Header h3 = new Header("header3", "value3"); + HTTPHeader h1 = new HTTPHeader("header1", "value1"); + HTTPHeader h2 = new HTTPHeader("header2", "value2"); + HTTPHeader h3 = new HTTPHeader("header3", "value3"); @BeforeEach void setUp() { @@ -42,7 +42,7 @@ void addHeaderDuplicateAdd() { @Test @DisplayName("Happy path add headers as list.") void addHeaders() { - List
    headersList = Arrays.asList(h1, h2, h3); + List headersList = Arrays.asList(h1, h2, h3); clientHeaders.addHeaders(headersList); Assertions.assertEquals(3, clientHeaders.headers().size()); Assertions.assertEquals(headersList, clientHeaders.headers()); @@ -51,7 +51,7 @@ void addHeaders() { @Test @DisplayName("Happy path add headers as list.") void addHeadersDuplicateAdd() { - List
    headersList = Arrays.asList(h1, h2, h1); + List headersList = Arrays.asList(h1, h2, h1); Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList)); } @@ -63,7 +63,7 @@ void removeHeader() { clientHeaders.addHeader(h3); clientHeaders.removeHeader(h2); Assertions.assertEquals(2, clientHeaders.headers().size()); - clientHeaders.removeHeader(new Header("header3", "value3")); + clientHeaders.removeHeader(new HTTPHeader("header3", "value3")); Assertions.assertEquals(1, clientHeaders.headers().size()); } @@ -78,8 +78,8 @@ void removeHeaderUnknown() { @Test @DisplayName("Happy path remove list of existing headers.") void removeHeaders() { - List
    headersToAddList = Arrays.asList(h1, h2, h3); - List
    headersToRemoveList = Arrays.asList(h2, h3); + List headersToAddList = Arrays.asList(h1, h2, h3); + List headersToRemoveList = Arrays.asList(h2, h3); clientHeaders.addHeaders(headersToAddList); clientHeaders.removeHeaders(headersToRemoveList); Assertions.assertEquals(1, clientHeaders.headers().size()); @@ -88,15 +88,15 @@ void removeHeaders() { @Test @DisplayName("Remove list containing unknown header.") void removeHeadersUnknown() { - List
    headersToAddList = Arrays.asList(h1, h3); - List
    headersToRemoveList = Arrays.asList(h2, h3); + List headersToAddList = Arrays.asList(h1, h3); + List headersToRemoveList = Arrays.asList(h2, h3); clientHeaders.addHeaders(headersToAddList); Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList)); } @Test void clearHeaders() { - List
    headersToAddList = Arrays.asList(h1, h3); + List headersToAddList = Arrays.asList(h1, h3); clientHeaders.addHeaders(headersToAddList); Assertions.assertEquals(2, clientHeaders.headers().size()); clientHeaders.clearHeaders(); diff --git a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilderTest.java b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilderTest.java index 8cd65e5d33..9024625faa 100644 --- a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilderTest.java +++ b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/utils/client/network/FhirRequestBuilderTest.java @@ -1,71 +1,51 @@ package org.hl7.fhir.r4b.utils.client.network; -import okhttp3.Headers; -import okhttp3.OkHttpClient; -import okhttp3.Request; + import org.hl7.fhir.r4b.model.OperationOutcome; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPHeaderUtil; +import org.hl7.fhir.utilities.http.HTTPRequest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.HashMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; class FhirRequestBuilderTest { - @Test - @DisplayName("Test default headers are added correctly.") - void addDefaultHeaders() { - Request.Builder request = new Request.Builder().url("http://www.google.com"); - FhirRequestBuilder.addDefaultHeaders(request, null); - - Map> headersMap = request.build().headers().toMultimap(); - Assertions.assertNotNull(headersMap.get("User-Agent"), "User-Agent header null."); - Assertions.assertEquals("hapi-fhir-tooling-client", headersMap.get("User-Agent").get(0), - "User-Agent header not populated with expected value \"hapi-fhir-tooling-client\"."); - - } @Test @DisplayName("Test resource format headers are added correctly.") - void addResourceFormatHeaders() { + void addResourceFormatHeadersGET() { String testFormat = "yaml"; - Request.Builder request = new Request.Builder().url("http://www.google.com"); - FhirRequestBuilder.addResourceFormatHeaders(request, testFormat); + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET); + + Iterable headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat); - Map> headersMap = request.build().headers().toMultimap(); + 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 + "\"."); + Assertions.assertNull(headersMap.get("Content-Type"), "Content-Type header null."); } @Test - @DisplayName("Test a list of provided headers are added correctly.") - void addHeaders() { - String headerName1 = "headerName1"; - String headerValue1 = "headerValue1"; - String headerName2 = "headerName2"; - String headerValue2 = "headerValue2"; - - Headers headers = new Headers.Builder().add(headerName1, headerValue1).add(headerName2, headerValue2).build(); - - Request.Builder request = new Request.Builder().url("http://www.google.com"); - FhirRequestBuilder.addHeaders(request, headers); - - Map> headersMap = request.build().headers().toMultimap(); - Assertions.assertNotNull(headersMap.get(headerName1), headerName1 + " header null."); - Assertions.assertEquals(headerValue1, headersMap.get(headerName1).get(0), - headerName1 + " header not populated with expected value " + headerValue1 + "."); - Assertions.assertNotNull(headersMap.get(headerName2), headerName2 + " header null."); - Assertions.assertEquals(headerValue2, headersMap.get(headerName2).get(0), - headerName2 + " header not populated with expected value " + headerValue2 + "."); + @DisplayName("Test resource format headers are added correctly (POST).") + void addResourceFormatHeadersPOST() { + String testFormat = "yaml"; + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST); + + Iterable 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 @@ -115,7 +95,7 @@ void hasErrorTestNoErrors() { @DisplayName("Test that getLocationHeader returns header for 'location'.") void getLocationHeaderWhenOnlyLocationIsSet() { final String expectedLocationHeader = "location_header_value"; - Headers headers = new Headers.Builder().add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader).build(); + Iterable headers = List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader)); Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); } @@ -123,8 +103,7 @@ void getLocationHeaderWhenOnlyLocationIsSet() { @DisplayName("Test that getLocationHeader returns header for 'content-location'.") void getLocationHeaderWhenOnlyContentLocationIsSet() { final String expectedContentLocationHeader = "content_location_header_value"; - Headers headers = new Headers.Builder() - .add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader).build(); + Iterable headers = List.of(new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)); Assertions.assertEquals(expectedContentLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); } @@ -133,15 +112,17 @@ void getLocationHeaderWhenOnlyContentLocationIsSet() { void getLocationHeaderWhenLocationAndContentLocationAreSet() { final String expectedLocationHeader = "location_header_value"; final String expectedContentLocationHeader = "content_location_header_value"; - Headers headers = new Headers.Builder().add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader) - .add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader).build(); + Iterable headers = List.of( + new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader), + new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) + ); Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); } @Test @DisplayName("Test that getLocationHeader returns null when no location available.") void getLocationHeaderWhenNoLocationSet() { - Headers headers = new Headers.Builder().build(); - Assertions.assertNull(FhirRequestBuilder.getLocationHeader(headers)); + + Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList())); } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/pom.xml b/org.hl7.fhir.r5/pom.xml index b441c2f4c3..e17a32aa96 100644 --- a/org.hl7.fhir.r5/pom.xml +++ b/org.hl7.fhir.r5/pom.xml @@ -126,6 +126,12 @@ + + org.assertj + assertj-core + test + + com.fasterxml.jackson.core jackson-databind diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java index 93b8a9f193..3db26b7e74 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/fhirpath/FHIRPathEngine.java @@ -604,7 +604,7 @@ public TypeDetails checkOnTypes(Object appContext, String resourceType, List warnings) throws FHIRLexerException, PathEngineException, DefinitionException { typeWarnings.clear(); TypeDetails res = executeType(new ExecutionTypeContext(appContext, resourceType, types, types), types, expr, null, true, false, expr); @@ -612,6 +612,13 @@ public TypeDetails checkOnTypes(Object appContext, String resourceType, TypeDeta return res; } + public TypeDetails checkOnTypes(Object appContext, String resourceType, TypeDetails types, ExpressionNode expr, List warnings, boolean canBeNone) throws FHIRLexerException, PathEngineException, DefinitionException { + typeWarnings.clear(); + TypeDetails res = executeType(new ExecutionTypeContext(appContext, resourceType, types, types), types, expr, null, true, canBeNone, expr); + warnings.addAll(typeWarnings); + return res; + } + /** * check that paths referred to in the ExpressionNode are valid * @@ -6585,7 +6592,7 @@ public String takeLog() { /** given an element definition in a profile, what element contains the differentiating fixed - * for the element, given the differentiating expresssion. The expression is only allowed to + * for the element, given the differentiating expression. The expression is only allowed to * use a subset of FHIRPath * * @param profile diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java index 3e63ac0a8a..daa378a5b6 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java @@ -1,22 +1,14 @@ package org.hl7.fhir.r5.terminologies; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Base64; import java.util.Date; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -156,9 +148,9 @@ public void commit(String token) throws IOException { // post it to String url = "https://tx.fhir.org/post/tx-cache/"+ghOrg+"/"+ghRepo+"/"+ghBranch+".zip"; System.out.println("Sending tx-cache to "+url+" ("+Utilities.describeSize(bs.toByteArray().length)+")"); - HTTPResult res = ManagedWebAccess.builder() + HTTPResult res = ManagedWebAccess.accessor() .withBasicAuth(token.substring(0, token.indexOf(':')), token.substring(token.indexOf(':') + 1)) - .withAccept("application/zip").put(url, bs.toByteArray(), null); + .put(url, bs.toByteArray(), null, "application/zip"); if (res.getCode() >= 300) { System.out.println("sending cache failed: "+res.getCode()); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientR5.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientR5.java index 063708bb81..b0f2f7b467 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientR5.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientR5.java @@ -2,7 +2,6 @@ import java.net.URISyntaxException; import java.util.EnumSet; -import java.util.HashMap; import java.util.Map; /* @@ -41,13 +40,10 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r5.model.CapabilityStatement; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.Parameters; -import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.TerminologyCapabilities; import org.hl7.fhir.r5.model.ValueSet; -import org.hl7.fhir.r5.terminologies.client.ITerminologyClient; import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager.ITerminologyClientFactory; -import org.hl7.fhir.r5.terminologies.client.TerminologyClientR5.TerminologyClientR5Factory; import org.hl7.fhir.r5.utils.client.FHIRToolingClient; import org.hl7.fhir.r5.utils.client.network.ClientHeaders; import org.hl7.fhir.utilities.FhirPublication; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/FHIRToolingClient.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/FHIRToolingClient.java index a1a112b963..cfc5e39808 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/FHIRToolingClient.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/FHIRToolingClient.java @@ -1,12 +1,11 @@ package org.hl7.fhir.r5.utils.client; -import okhttp3.Headers; -import okhttp3.internal.http2.Header; +import lombok.Getter; +import lombok.Setter; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.OperationOutcome; import org.hl7.fhir.r5.model.Resource; -import org.hl7.fhir.r5.utils.client.EFhirClientException; /* Copyright (c) 2011+, HL7, Inc. @@ -45,7 +44,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.utilities.FHIRBaseToolingClient; import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; +import org.hl7.fhir.utilities.http.HTTPHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,18 +92,24 @@ public class FHIRToolingClient extends FHIRBaseToolingClient { private String base; private ResourceAddress resourceAddress; + @Setter private ResourceFormat preferredResourceFormat; private int maxResultSetSize = -1;//_count private CapabilityStatement capabilities; + @Getter + @Setter private Client client = new Client(); - private ArrayList
    headers = new ArrayList<>(); - private String username; - private String password; + private List headers = new ArrayList<>(); + + @Setter + @Getter private String userAgent; + @Setter + private String acceptLanguage; - private String acceptLang; - private String contentLang; + @Setter + private String contentLanguage; private int useCount; @@ -124,22 +129,10 @@ public void initialize(String baseServiceUrl) throws URISyntaxException { this.maxResultSetSize = -1; } - public Client getClient() { - return client; - } - - public void setClient(Client client) { - this.client = client; - } - public String getPreferredResourceFormat() { return preferredResourceFormat.getHeader(); } - public void setPreferredResourceFormat(ResourceFormat resourceFormat) { - preferredResourceFormat = resourceFormat; - } - public int getMaximumRecordCount() { return maxResultSetSize; } @@ -410,10 +403,14 @@ public OperationOutcome validate(Class resourceClass, T } /** - * Helper method to prevent nesting of previously thrown EFhirClientExceptions + * Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of + * EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the + * cause. * - * @param e - * @throws EFhirClientException + * @param code The EFhirClientException code. + * @param message The EFhirClientException message. + * @param e The exception. + * @throws EFhirClientException representing the exception. */ protected void handleException(int code, String message, Exception e) throws EFhirClientException { if (e instanceof EFhirClientException) { @@ -427,8 +424,8 @@ protected void handleException(int code, String message, Exception e) throws EFh * Helper method to determine whether desired resource representation * is Json or XML. * - * @param format - * @return + * @param format The format + * @return true if the format is JSON, false otherwise */ protected boolean isJson(String format) { boolean isJson = false; @@ -573,22 +570,6 @@ public ConceptMap updateClosure(String name, Coding coding) { return result == null ? null : (ConceptMap) result.getPayload(); } - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public long getTimeout() { return client.getTimeout(); } @@ -613,51 +594,27 @@ public void setRetryCount(int retryCount) { client.setRetryCount(retryCount); } - public void setClientHeaders(ArrayList
    headers) { - this.headers = headers; + public void setClientHeaders(Iterable headers) { + this.headers = new ArrayList<>(); + headers.forEach(this.headers::add); } - private Headers generateHeaders(boolean hasBody) { - Headers.Builder builder = new Headers.Builder(); - // Add basic auth header if it exists - if (basicAuthHeaderExists()) { - builder.add(getAuthorizationHeader().toString()); - } + private Iterable generateHeaders(boolean hasBody) { // Add any other headers - if(this.headers != null) { - this.headers.forEach(header -> builder.add(header.toString())); - } + List headers = new ArrayList<>(this.headers); if (!Utilities.noString(userAgent)) { - builder.add("User-Agent: "+userAgent); + headers.add(new HTTPHeader("User-Agent",userAgent)); } - if (!Utilities.noString(acceptLang)) { - builder.add("Accept-Language: "+acceptLang); + if (!Utilities.noString(acceptLanguage)) { + headers.add(new HTTPHeader("Accept-Language", acceptLanguage)); } - if (hasBody && !Utilities.noString(contentLang)) { - builder.add("Content-Language: "+contentLang); + if (hasBody && !Utilities.noString(contentLanguage)) { + headers.add(new HTTPHeader("Content-Language", contentLanguage)); } - return builder.build(); - } - - public boolean basicAuthHeaderExists() { - return (username != null) && (password != null); - } - - public Header getAuthorizationHeader() { - String usernamePassword = username + ":" + password; - String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes()); - return new Header("Authorization", "Basic " + base64usernamePassword); - } - - public String getUserAgent() { - return userAgent; - } - - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; + return headers; } public String getServerVersion() { @@ -671,14 +628,6 @@ public String getServerVersion() { return capabilities == null ? null : capabilities.getSoftware().getVersion(); } - public void setAcceptLanguage(String lang) { - this.acceptLang = lang; - } - - public void setContentLanguage(String lang) { - this.contentLang = lang; - } - public Bundle search(String type, String criteria) { recordUse(); return fetchFeed(Utilities.pathURL(base, type+criteria)); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/Client.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/Client.java index 1895c2ccf3..7eea93a942 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/Client.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/Client.java @@ -1,16 +1,18 @@ package org.hl7.fhir.r5.utils.client.network; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; +import lombok.Getter; +import lombok.Setter; + import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.utils.client.EFhirClientException; import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.hl7.fhir.utilities.http.HTTPHeader; import java.io.IOException; import java.net.URI; +import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -18,93 +20,60 @@ public class Client { public static final String DEFAULT_CHARSET = "UTF-8"; private static final long DEFAULT_TIMEOUT = 5000; + @Getter @Setter private ToolingClientLogger logger; - private FhirLoggingInterceptor fhirLoggingInterceptor; + + @Setter @Getter private int retryCount; + @Setter @Getter private long timeout = DEFAULT_TIMEOUT; - private byte[] payload; - private String base; - - public String getBase() { - return base; - } - public void setBase(String base) { - this.base = base; - } - - public ToolingClientLogger getLogger() { - return logger; - } - - public void setLogger(ToolingClientLogger logger) { - this.logger = logger; - this.fhirLoggingInterceptor = new FhirLoggingInterceptor(logger); - } - - public int getRetryCount() { - return retryCount; - } - - public void setRetryCount(int retryCount) { - this.retryCount = retryCount; - } - - public long getTimeout() { - return timeout; - } - - public void setTimeout(long timeout) { - this.timeout = timeout; - } + @Setter @Getter + private String base; public ResourceRequest issueOptionsRequest(URI optionsUri, String resourceFormat, String message, long timeout) throws IOException { - this.payload = null; - Request.Builder request = new Request.Builder() - .method("OPTIONS", null) - .url(optionsUri.toURL()); - - return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(optionsUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.OPTIONS); + return executeFhirRequest(request, resourceFormat, Collections.emptyList(), message, retryCount, timeout); } public ResourceRequest issueGetResourceRequest(URI resourceUri, String resourceFormat, - Headers headers, + Iterable headers, String message, long timeout) throws IOException { - this.payload = null; - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } - public int tester(int trytry) { - return 5; - } public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, long timeout) throws IOException { - return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout); + return issuePutRequest(resourceUri, payload, resourceFormat, Collections.emptyList(), message, timeout); } public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, - Headers headers, + Iterable headers, String message, long timeout) throws IOException { if (payload == null) throw new EFhirClientException(0, "PUT requests require a non-null payload"); - this.payload = payload; - RequestBody body = RequestBody.create(payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .put(body); + + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.PUT) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } @@ -114,37 +83,38 @@ public ResourceRequest issuePostRequest(URI resourceUri, 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(0, "POST requests require a non-null payload"); - this.payload = payload; - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .post(body); + + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); } public boolean issueDeleteRequest(URI resourceUri) throws IOException { - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .delete(); - return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest(); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.DELETE); + return executeFhirRequest(request, null, Collections.emptyList(), null, retryCount, timeout).isSuccessfulRequest(); } public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException { - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()); - - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.GET); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle issuePostFeedRequest(URI resourceUri, @@ -154,57 +124,63 @@ public Bundle issuePostFeedRequest(URI resourceUri, String resourceFormat) throws IOException { String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; byte[] payload = ByteUtils.encodeFormSubmission(parameters, resourceName, resource, boundary); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .post(body); - return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); + return executeBundleRequest(request, resourceFormat, Collections.emptyList(), null, retryCount, timeout); } public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, - Headers headers, + Iterable headers, String message, int timeout) throws IOException { if (payload == null) throw new EFhirClientException(0, "POST requests require a non-null payload"); - RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload); - Request.Builder request = new Request.Builder() - .url(resourceUri.toURL()) - .post(body); + HTTPRequest request = new HTTPRequest() + .withUrl(resourceUri.toURL()) + .withMethod(HTTPRequest.HttpMethod.POST) + .withBody(payload) + .withContentType(getContentTypeWithDefaultCharset(resourceFormat)); return executeBundleRequest(request, resourceFormat, headers, message, retryCount, timeout); } - public Bundle executeBundleRequest(Request.Builder request, - String resourceFormat, - Headers headers, - String message, - int retryCount, - long timeout) throws IOException { + private static String getContentTypeWithDefaultCharset(String resourceFormat) { + return resourceFormat + ";charset=" + DEFAULT_CHARSET; + } + + public Bundle executeBundleRequest(HTTPRequest request, + String resourceFormat, + Iterable headers, + String message, + int retryCount, + long timeout) throws IOException { return new FhirRequestBuilder(request, base) - .withLogger(fhirLoggingInterceptor) + .withLogger(logger) .withResourceFormat(resourceFormat) .withRetryCount(retryCount) .withMessage(message) - .withHeaders(headers == null ? new Headers.Builder().build() : headers) + .withHeaders(headers == null ? Collections.emptyList() : headers) .withTimeout(timeout, TimeUnit.MILLISECONDS) .executeAsBatch(); } - public 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(fhirLoggingInterceptor) + .withLogger(logger) .withResourceFormat(resourceFormat) .withRetryCount(retryCount) .withMessage(message) - .withHeaders(headers == null ? new Headers.Builder().build() : headers) + .withHeaders(headers == null ? Collections.emptyList() : headers) .withTimeout(timeout, TimeUnit.MILLISECONDS) .execute(); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ClientHeaders.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ClientHeaders.java index efe62ea30c..14e20fa397 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ClientHeaders.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ClientHeaders.java @@ -1,7 +1,8 @@ package org.hl7.fhir.r5.utils.client.network; -import okhttp3.internal.http2.Header; + import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.http.HTTPHeader; import java.util.ArrayList; import java.util.List; @@ -15,30 +16,31 @@ */ public class ClientHeaders { - private final ArrayList
    headers; + private final List headers; public ClientHeaders() { this.headers = new ArrayList<>(); } - public ClientHeaders(ArrayList
    headers) { - this.headers = headers; + public ClientHeaders(List headers) { + + this.headers = new ArrayList<>(headers); } - public ArrayList
    headers() { + public List headers() { return headers; } /** * Add a header to the list of stored headers for network operations. * - * @param header {@link Header} to add to the list. + * @param header {@link HTTPHeader} to add to the list. * @throws FHIRException if the header being added is a duplicate. */ - public ClientHeaders addHeader(Header header) throws FHIRException { + public ClientHeaders addHeader(HTTPHeader header) throws FHIRException { if (headers.contains(header)) { - throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", " - + header.value + ">."); + throw new FHIRException("Attempting to add duplicate header, <" + header.getName() + ", " + + header.getValue() + ">."); } headers.add(header); return this; @@ -47,39 +49,39 @@ public ClientHeaders addHeader(Header header) throws FHIRException { /** * Add a header to the list of stored headers for network operations. * - * @param headerList {@link List} of {@link Header} to add. + * @param headerList {@link List} of {@link HTTPHeader} to add. * @throws FHIRException if any of the headers being added is a duplicate. */ - public ClientHeaders addHeaders(List
    headerList) throws FHIRException { + public ClientHeaders addHeaders(List headerList) throws FHIRException { headerList.forEach(this::addHeader); return this; } /** * Removes the passed in header from the list of stored headers. - * @param header {@link Header} to remove from the list. + * @param header {@link HTTPHeader} to remove from the list. * @throws FHIRException if the header passed in does not exist within the stored list. */ - public ClientHeaders removeHeader(Header header) throws FHIRException { + public ClientHeaders removeHeader(HTTPHeader header) throws FHIRException { if (!headers.remove(header)) { - throw new FHIRException("Attempting to remove header, <" + header.name + ", " - + header.value + ">, from GenericClientHeaders that is not currently stored."); + throw new FHIRException("Attempting to remove header, <" + header.getName() + ", " + + header.getValue() + ">, from GenericClientHeaders that is not currently stored."); } return this; } /** * Removes the passed in headers from the list of stored headers. - * @param headerList {@link List} of {@link Header} to remove. + * @param headerList {@link List} of {@link HTTPHeader} to remove. * @throws FHIRException if any of the headers passed in does not exist within the stored list. */ - public ClientHeaders removeHeaders(List
    headerList) throws FHIRException { + public ClientHeaders removeHeaders(List headerList) throws FHIRException { headerList.forEach(this::removeHeader); return this; } /** - * Clears all stored {@link Header}. + * Clears all stored {@link HTTPHeader}. */ public ClientHeaders clearHeaders() { headers.clear(); @@ -89,7 +91,7 @@ public ClientHeaders clearHeaders() { @Override public String toString() { return this.headers.stream() - .map(header -> "\t" + header.name + ":" + header.value) + .map(header -> "\t" + header.getName() + ":" + header.getValue()) .collect(Collectors.joining(",\n", "{\n", "\n}")); } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirLoggingInterceptor.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirLoggingInterceptor.java deleted file mode 100644 index 1314e6c8a7..0000000000 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirLoggingInterceptor.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.hl7.fhir.r5.utils.client.network; - -import okhttp3.*; -import okio.Buffer; - -import org.hl7.fhir.utilities.ToolingClientLogger; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class FhirLoggingInterceptor implements Interceptor { - - private ToolingClientLogger logger; - - public FhirLoggingInterceptor(ToolingClientLogger logger) { - this.logger = logger; - } - - public FhirLoggingInterceptor setLogger(ToolingClientLogger logger) { - this.logger = logger; - return this; - } - - @Override - public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException { - // Log Request - Request request = chain.request(); - List hdrs = new ArrayList<>(); - for (String s : request.headers().toString().split("\\n")) { - hdrs.add(s.trim()); - } - byte[] cnt = null; - if (request.body() != null) { - Buffer buf = new Buffer(); - request.body().writeTo(buf); - cnt = buf.readByteArray(); - } - if (logger != null) { - logger.logRequest(request.method(), request.url().toString(), hdrs, cnt); - } - - // Log Response - Response response = null; - response = chain.proceed(chain.request()); - - MediaType contentType = null; - byte[] bodyBytes = null; - if (response.body() != null) { - contentType = response.body().contentType(); - bodyBytes = response.body().bytes(); - } - - // Get Headers as List - List headerList = new ArrayList<>(); - Map> headerMap = response.headers().toMultimap(); - headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value))); - - if (logger != null) { - long responseTimeInMillis = response.receivedResponseAtMillis() - response.sentRequestAtMillis(); - logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, responseTimeInMillis); - } - - // Reading byte[] clears body. Need to recreate. - ResponseBody body = ResponseBody.create(bodyBytes, contentType); - return response.newBuilder().body(body).build(); - } -} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilder.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilder.java index 49ee7d8816..750bce23f2 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilder.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilder.java @@ -1,14 +1,12 @@ package org.hl7.fhir.r5.utils.client.network; import java.io.IOException; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.formats.IParser; import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.formats.XmlParser; @@ -20,32 +18,20 @@ import org.hl7.fhir.r5.utils.client.EFhirClientException; import org.hl7.fhir.r5.utils.client.ResourceFormat; import org.hl7.fhir.utilities.MimeType; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.settings.FhirSettings; +import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.*; import org.hl7.fhir.utilities.xhtml.XhtmlUtils; -import okhttp3.Authenticator; -import okhttp3.Credentials; -import okhttp3.Headers; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - public class FhirRequestBuilder { - protected static final String HTTP_PROXY_USER = "http.proxyUser"; - protected static final String HTTP_PROXY_PASS = "http.proxyPassword"; - protected static final String HEADER_PROXY_AUTH = "Proxy-Authorization"; + protected static final String DEFAULT_CHARSET = "UTF-8"; + protected static final String LOCATION_HEADER = "location"; protected static final String CONTENT_LOCATION_HEADER = "content-location"; - protected static final String DEFAULT_CHARSET = "UTF-8"; - /** - * The singleton instance of the HttpClient, used for all requests. - */ - private static OkHttpClient okHttpClient; - private final Request.Builder httpRequest; + + private final HTTPRequest httpRequest; private String resourceFormat = null; - private Headers headers = null; + private Iterable headers = null; private String message = null; private int retryCount = 1; /** @@ -58,69 +44,57 @@ public class FhirRequestBuilder { private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; /** - * {@link FhirLoggingInterceptor} for log output. + * {@link ToolingClientLogger} for log output. */ - private FhirLoggingInterceptor logger = null; + private ToolingClientLogger logger = null; + private String source; - public FhirRequestBuilder(Request.Builder httpRequest, String source) { - this.httpRequest = httpRequest; + public FhirRequestBuilder(HTTPRequest httpRequest, String source) { this.source = source; + this.httpRequest = httpRequest; } /** - * Adds necessary default headers, formatting headers, and any passed in {@link Headers} to the passed in - * {@link okhttp3.Request.Builder} + * Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s to the passed in + * {@link HTTPRequest} * - * @param request {@link okhttp3.Request.Builder} to add headers to. + * @param request {@link HTTPRequest} to add headers to. * @param format Expected {@link Resource} format. - * @param headers Any additional {@link Headers} to add to the request. + * @param headers Any additional {@link HTTPHeader}s to add to the request. */ - protected static void formatHeaders(Request.Builder request, String format, Headers headers) { - addDefaultHeaders(request, headers); - if (format != null) addResourceFormatHeaders(request, format); - if (headers != null) addHeaders(request, headers); - } + protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable 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"); - } + if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add); + if (headers != null) headers.forEach(allHeaders::add); + return request.withHeaders(allHeaders); } /** * Adds necessary headers for the given resource format provided. * - * @param request {@link Request.Builder} to add default headers to. + * @param httpRequest {@link HTTPRequest} to add default headers to. + * @param format Expected {@link Resource} format. */ - protected static void addResourceFormatHeaders(Request.Builder request, String format) { - request.addHeader("Accept", format); - if (Utilities.existsInList(request.getMethod$okhttp(), "POST", "PUT")) { - request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); + protected static Iterable 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; } /** - * Iterates through the passed in {@link Headers} and adds them to the provided {@link Request.Builder}. - * - * @param request {@link Request.Builder} to add headers to. - * @param headers {@link Headers} to add to request. - */ - protected static void addHeaders(Request.Builder request, Headers headers) { - headers.forEach(header -> request.addHeader(header.getFirst(), header.getSecond())); - } - - /** - * Returns true if any of the {@link org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent} within the - * provided {@link OperationOutcome} have an {@link org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity} of - * {@link org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity#ERROR} or - * {@link org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity#FATAL} + * Returns true if any of the {@link OperationOutcome.OperationOutcomeIssueComponent} within the + * provided {@link OperationOutcome} have an {@link OperationOutcome.IssueSeverity} of + * {@link OperationOutcome.IssueSeverity#ERROR} or + * {@link OperationOutcome.IssueSeverity#FATAL} * * @param oo {@link OperationOutcome} to evaluate. * @return {@link Boolean#TRUE} if an error exists. @@ -131,71 +105,8 @@ protected static boolean hasError(OperationOutcome oo) { || issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL)); } - /** - * Extracts the 'location' header from the passes in {@link Headers}. If no value for 'location' exists, the - * value for 'content-location' is returned. If neither header exists, we return null. - * - * @param headers {@link Headers} to evaluate - * @return {@link String} header value, or null if no location headers are set. - */ - protected static String getLocationHeader(Headers headers) { - Map> headerMap = headers.toMultimap(); - if (headerMap.containsKey(LOCATION_HEADER)) { - return headerMap.get(LOCATION_HEADER).get(0); - } else if (headerMap.containsKey(CONTENT_LOCATION_HEADER)) { - return headerMap.get(CONTENT_LOCATION_HEADER).get(0); - } else { - return null; - } - } - - /** - * We only ever want to have one copy of the HttpClient kicking around at any given time. If we need to make changes - * to any configuration, such as proxy settings, timeout, caches, etc, we can do a per-call configuration through - * the {@link OkHttpClient#newBuilder()} method. That will return a builder that shares the same connection pool, - * dispatcher, and configuration with the original client. - *

    - * The {@link OkHttpClient} uses the proxy auth properties set in the current system properties. The reason we don't - * set the proxy address and authentication explicitly, is due to the fact that this class is often used in conjunction - * with other http client tools which rely on the system.properties settings to determine proxy settings. It's easier - * to keep the method consistent across the board. ...for now. - * - * @return {@link OkHttpClient} instance - */ - protected OkHttpClient getHttpClient() { - if (FhirSettings.isProhibitNetworkAccess()) { - throw new FHIRException("Network Access is prohibited in this context"); - } - - if (okHttpClient == null) { - okHttpClient = new OkHttpClient(); - } - - Authenticator proxyAuthenticator = getAuthenticator(); - - OkHttpClient.Builder builder = okHttpClient.newBuilder(); - if (logger != null) builder.addInterceptor(logger); - builder.addInterceptor(new RetryInterceptor(retryCount)); - return builder.connectTimeout(timeout, timeoutUnit) - .writeTimeout(timeout, timeoutUnit) - .readTimeout(timeout, timeoutUnit) - .proxyAuthenticator(proxyAuthenticator) - .build(); - } - - @Nonnull - private static Authenticator getAuthenticator() { - return (route, response) -> { - final String httpProxyUser = System.getProperty(HTTP_PROXY_USER); - final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS); - if (httpProxyUser != null && httpProxyPass != null) { - String credential = Credentials.basic(httpProxyUser, httpProxyPass); - return response.request().newBuilder() - .header(HEADER_PROXY_AUTH, credential) - .build(); - } - return response.request().newBuilder().build(); - }; + protected ManagedFhirWebAccessor getManagedWebAccessor() { + return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger); } public FhirRequestBuilder withResourceFormat(String resourceFormat) { @@ -203,7 +114,7 @@ public FhirRequestBuilder withResourceFormat(String resourceFormat) { return this; } - public FhirRequestBuilder withHeaders(Headers headers) { + public FhirRequestBuilder withHeaders(Iterable headers) { this.headers = headers; return this; } @@ -218,7 +129,7 @@ public FhirRequestBuilder withRetryCount(int retryCount) { return this; } - public FhirRequestBuilder withLogger(FhirLoggingInterceptor logger) { + public FhirRequestBuilder withLogger(ToolingClientLogger logger) { this.logger = logger; return this; } @@ -229,20 +140,16 @@ public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) { return this; } - protected Request buildRequest() { - return httpRequest.build(); - } - public ResourceRequest execute() throws IOException { - formatHeaders(httpRequest, resourceFormat, headers); - Response response = getHttpClient().newCall(httpRequest.build()).execute(); + HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers); + HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);//getHttpClient().newCall(httpRequest.build()).execute(); T resource = unmarshalReference(response, resourceFormat, null); - return new ResourceRequest(resource, response.code(), getLocationHeader(response.headers())); + return new ResourceRequest(resource, response.getCode(), getLocationHeader(response.getHeaders())); } public Bundle executeAsBatch() throws IOException { - formatHeaders(httpRequest, resourceFormat, null); - Response response = getHttpClient().newCall(httpRequest.build()).execute(); + HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null); + HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders); return unmarshalFeed(response, resourceFormat); } @@ -250,12 +157,12 @@ public Bundle executeAsBatch() throws IOException { * Unmarshalls a resource from the response stream. */ @SuppressWarnings("unchecked") - protected T unmarshalReference(Response response, String format, String resourceType) { - int code = response.code(); + protected T unmarshalReference(HTTPResult response, String format, String resourceType) { + int code = response.getCode(); boolean ok = code >= 200 && code < 300; - if (response.body() == null) { + if (response.getContent() == null) { if (!ok) { - throw new EFhirClientException(code, response.message()); + throw new EFhirClientException(code, response.getMessage()); } else { return null; } @@ -264,9 +171,9 @@ protected T unmarshalReference(Response response, String fo Resource resource = null; try { - body = response.body().string(); - String ct = response.header("Content-Type"); - if (ct == null) { + body = response.getContentAsString(); + String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type"); + if (contentType == null) { if (ok) { resource = getParser(format).parse(body); } else { @@ -275,10 +182,10 @@ protected T unmarshalReference(Response response, String fo resource = OperationOutcomeUtilities.outcomeFromTextError(body); } } else { - if (ct.contains(";")) { - ct = ct.substring(0, ct.indexOf(";")); + if (contentType.contains(";")) { + contentType = contentType.substring(0, contentType.indexOf(";")); } - switch (ct) { + switch (contentType) { case "application/json": case "application/fhir+json": if (!format.contains("json")) { @@ -298,10 +205,10 @@ protected T unmarshalReference(Response response, String fo resource = OperationOutcomeUtilities.outcomeFromTextError(body); break; case "text/html" : - resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.body().string(), source)); + resource = OperationOutcomeUtilities.outcomeFromTextError(XhtmlUtils.convertHtmlToText(response.getContentAsString(), source)); break; default: // not sure what else to do? - System.out.println("Got content-type '"+ct+"' from "+source); + System.out.println("Got content-type '"+contentType+"' from "+source); System.out.println(body); resource = OperationOutcomeUtilities.outcomeFromTextError(body); } @@ -336,14 +243,14 @@ protected T unmarshalReference(Response response, String fo /** * Unmarshalls Bundle from response stream. */ - protected Bundle unmarshalFeed(Response response, String format) { + protected Bundle unmarshalFeed(HTTPResult response, String format) { return unmarshalReference(response, format, "Bundle"); } /** * Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is * provided...because reasons. - *

    + *

    * Currently supports only "json" and "xml" formats. * * @param format One of "json" or "xml". @@ -362,4 +269,19 @@ protected IParser getParser(String format) { throw new EFhirClientException(0, "Invalid format: " + format); } } + + /** + * Extracts the 'location' header from the passed headers. If no value for 'location' exists, the + * value for 'content-location' is returned. If neither header exists, we return null. + * + * @param headers Headers to search for 'location' or 'content-location'. + */ + protected static String getLocationHeader(Iterable headers) { + String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER); + + if (locationHeader != null) { + return locationHeader; + } + return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER); + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/RetryInterceptor.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/RetryInterceptor.java deleted file mode 100644 index d6f4e2de11..0000000000 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/RetryInterceptor.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.hl7.fhir.r5.utils.client.network; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import javax.annotation.Nonnull; - -import java.io.IOException; - -/** - * An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the number of times we retry a to execute a - * given request, before reporting a failure. This includes unsuccessful return codes and timeouts. - */ -public class RetryInterceptor implements Interceptor { - - // Delay between retying failed requests, in millis - private final long RETRY_TIME = 2000; - - // Maximum number of times to retry the request before failing - private final int maxRetry; - - // Internal counter for tracking the number of times we've tried this request - private int retryCounter = 0; - - public RetryInterceptor(int maxRetry) { - this.maxRetry = maxRetry; - } - - @Override - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = null; - - do { - try { - // If we are retrying a failed request that failed due to a bad response from the server, we must close it first - if (response != null) { -// System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code()) -// + "> from url -> " + chain.request().url() + "."); - response.close(); - } - // System.out.println(chain.request().method() + " attempt <" + (retryCounter + 1) + "> to url -> " + chain.request().url()); - response = chain.proceed(request); - } catch (IOException e) { - try { - // Include a small break in between requests. - Thread.sleep(RETRY_TIME); - } catch (InterruptedException e1) { - System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" + retryCounter + ">"); - } - } finally { - retryCounter++; - } - } while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1)); - - /* - * if something has gone wrong, and we are unable to complete the request, we still need to initialize the return - * response so we don't get a null pointer exception. - */ - return response != null ? response : chain.proceed(request); - } - -} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/FHIRToolingClientTest.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/FHIRToolingClientTest.java index 6987d1fc05..caa44aa5e5 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/FHIRToolingClientTest.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/FHIRToolingClientTest.java @@ -5,6 +5,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; @@ -22,17 +23,14 @@ import org.hl7.fhir.r5.utils.client.network.Client; import org.hl7.fhir.r5.utils.client.network.ResourceRequest; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.hl7.fhir.utilities.http.HTTPHeader; import org.hl7.fhir.utilities.settings.FhirSettings; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; +import org.mockito.*; import okhttp3.Headers; -import okhttp3.Request; -import okhttp3.internal.http2.Header; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.times; @@ -41,46 +39,56 @@ class FHIRToolingClientTest { String TX_ADDR = FhirSettings.getTxFhirDevelopment(); - Header h1 = new Header("header1", "value1"); - Header h2 = new Header("header2", "value2"); - Header h3 = new Header("header3", "value3"); + HTTPHeader h1 = new HTTPHeader("header1", "value1"); + HTTPHeader h2 = new HTTPHeader("header2", "value2"); + HTTPHeader h3 = new HTTPHeader("header3", "value3"); + + HTTPHeader agentHeader = new HTTPHeader("User-Agent", "fhir/test-cases"); private Client mockClient; private FHIRToolingClient toolingClient; + @Captor + private ArgumentCaptor> 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(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())) + Mockito.any(Iterable.class), Mockito.anyString(), Mockito.anyLong())) .thenReturn(resourceResourceRequest); //PUT Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())) .thenReturn(resourceResourceRequest); //POST Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyLong())) .thenReturn(resourceResourceRequest); Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.contains("validate"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.contains("validate"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location")); //BUNDLE REQ - Mockito.when(mockClient.executeBundleRequest(Mockito.any(Request.Builder.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong())) + Mockito.when(mockClient.executeBundleRequest(Mockito.any(HTTPRequest.class), Mockito.anyString(), + ArgumentMatchers.any(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong())) .thenReturn(generateBundle()); toolingClient = new FHIRToolingClient(TX_ADDR, "fhir/test-cases"); toolingClient.setClient(mockClient); } - private ArrayList

    getHeaders() { + 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(); @@ -140,36 +148,37 @@ private Observation generateObservation() { return observation; } - private void checkHeaders(Headers argumentCaptorValue) { - getHeaders().forEach(header -> { -// System.out.println("Checking header <" + header.component1().utf8() + ", " + header.component2().utf8() + ">"); - Assertions.assertEquals(argumentCaptorValue.get(header.component1().utf8()), header.component2().utf8()); + private void checkHeaders(Iterable argumentCaptorValue) { + List capturedHeaders = new ArrayList<>(); + argumentCaptorValue.forEach(capturedHeaders::add); + + getHeadersWithAgent().forEach(header -> { + assertTrue(capturedHeaders.contains(header)); }); } @Test void getTerminologyCapabilities() throws IOException { + Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location")); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getTerminologyCapabilities(); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getTerminologyCapabilitiesNotSupported() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); Exception exception = assertThrows(FHIRException.class, () -> { toolingClient.getTerminologyCapabilities(); @@ -180,24 +189,23 @@ void getTerminologyCapabilitiesNotSupported() throws IOException { @Test void getTerminologyCapabilitiesFailsForJSON() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) .thenThrow(new FHIRFormatError("dummy error")) .thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location")); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getTerminologyCapabilities(); Mockito.verify(mockClient, times(2)).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getTerminologyCapabilitiesStatementFailsForJSONandXML() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong())) .thenThrow(new FHIRFormatError("dummy error")) .thenThrow(new FHIRFormatError("dummy error 2")); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); @@ -210,10 +218,9 @@ void getTerminologyCapabilitiesStatementFailsForJSONandXML() throws IOException @Test void getCapabilitiesStatement() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); toolingClient.setClientHeaders(getHeaders()); @@ -222,18 +229,17 @@ void getCapabilitiesStatement() throws IOException { headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getCapabilitiesStatementFailsForJSON() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) .thenThrow(new FHIRFormatError("dummy error")) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCapabilitiesStatement(); @@ -241,14 +247,14 @@ void getCapabilitiesStatementFailsForJSON() throws IOException { headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); assertEquals(ResourceFormat.RESOURCE_XML.getHeader(), toolingClient.getPreferredResourceFormat()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getCapabilitiesStatementFailsForJSONandXML() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong())) .thenThrow(new FHIRFormatError("dummy error")) .thenThrow(new FHIRFormatError("dummy error 2")); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); @@ -261,17 +267,16 @@ void getCapabilitiesStatementFailsForJSONandXML() throws IOException { @Test void getCapabilitiesStatementQuick() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCapabilitiesStatementQuick(); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); @@ -280,19 +285,18 @@ void getCapabilitiesStatementQuick() throws IOException { @Test void getCapabilitiesStatementQuickFailsForJSON() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) .thenThrow(new FHIRFormatError("dummy error")) .thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location")); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCapabilitiesStatementQuick(); Mockito.verify(mockClient, times(2)).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); assertEquals(ResourceFormat.RESOURCE_XML.getHeader(), toolingClient.getPreferredResourceFormat()); @@ -301,11 +305,10 @@ void getCapabilitiesStatementQuickFailsForJSON() throws IOException { @Test void getCapabilitiesStatementQuickFailsForJSONandXML() throws IOException { Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(), - Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) + ArgumentMatchers.any(), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong())) .thenThrow(new FHIRFormatError("dummy error")) .thenThrow(new FHIRFormatError("dummy error 2")); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); assertEquals(ResourceFormat.RESOURCE_JSON.getHeader(), toolingClient.getPreferredResourceFormat()); Exception exception = assertThrows(FHIRException.class, () -> { @@ -317,63 +320,58 @@ void getCapabilitiesStatementQuickFailsForJSONandXML() throws IOException { @Test void read() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.read(Patient.class, "id"); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void vread() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.vread(Patient.class, "id", "version"); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void getCanonical() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.getCanonical(Patient.class, "canonicalURL"); Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void update() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.update(generatePatient()); Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } @Test void validate() throws IOException { - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); toolingClient.setClientHeaders(getHeaders()); toolingClient.validate(Patient.class, generatePatient(), "id"); Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()); - Headers argumentCaptorValue = headersArgumentCaptor.getValue(); + Iterable argumentCaptorValue = headersArgumentCaptor.getValue(); checkHeaders(argumentCaptorValue); } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientHeadersTest.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientHeadersTest.java index 7f102bf735..2dd985409d 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientHeadersTest.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientHeadersTest.java @@ -4,20 +4,19 @@ import java.util.List; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.http.HTTPHeader; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import okhttp3.internal.http2.Header; - class ClientHeadersTest { ClientHeaders clientHeaders; - Header h1 = new Header("header1", "value1"); - Header h2 = new Header("header2", "value2"); - Header h3 = new Header("header3", "value3"); + HTTPHeader h1 = new HTTPHeader("header1", "value1"); + HTTPHeader h2 = new HTTPHeader("header2", "value2"); + HTTPHeader h3 = new HTTPHeader("header3", "value3"); @BeforeEach void setUp() { @@ -43,7 +42,7 @@ void addHeaderDuplicateAdd() { @Test @DisplayName("Happy path add headers as list.") void addHeaders() { - List
    headersList = Arrays.asList(h1, h2, h3); + List headersList = Arrays.asList(h1, h2, h3); clientHeaders.addHeaders(headersList); Assertions.assertEquals(3, clientHeaders.headers().size()); Assertions.assertEquals(headersList, clientHeaders.headers()); @@ -52,7 +51,7 @@ void addHeaders() { @Test @DisplayName("Happy path add headers as list.") void addHeadersDuplicateAdd() { - List
    headersList = Arrays.asList(h1, h2, h1); + List headersList = Arrays.asList(h1, h2, h1); Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList)); } @@ -64,7 +63,7 @@ void removeHeader() { clientHeaders.addHeader(h3); clientHeaders.removeHeader(h2); Assertions.assertEquals(2, clientHeaders.headers().size()); - clientHeaders.removeHeader(new Header("header3", "value3")); + clientHeaders.removeHeader(new HTTPHeader("header3", "value3")); Assertions.assertEquals(1, clientHeaders.headers().size()); } @@ -79,8 +78,8 @@ void removeHeaderUnknown() { @Test @DisplayName("Happy path remove list of existing headers.") void removeHeaders() { - List
    headersToAddList = Arrays.asList(h1, h2, h3); - List
    headersToRemoveList = Arrays.asList(h2, h3); + List headersToAddList = Arrays.asList(h1, h2, h3); + List headersToRemoveList = Arrays.asList(h2, h3); clientHeaders.addHeaders(headersToAddList); clientHeaders.removeHeaders(headersToRemoveList); Assertions.assertEquals(1, clientHeaders.headers().size()); @@ -89,15 +88,15 @@ void removeHeaders() { @Test @DisplayName("Remove list containing unknown header.") void removeHeadersUnknown() { - List
    headersToAddList = Arrays.asList(h1, h3); - List
    headersToRemoveList = Arrays.asList(h2, h3); + List headersToAddList = Arrays.asList(h1, h3); + List headersToRemoveList = Arrays.asList(h2, h3); clientHeaders.addHeaders(headersToAddList); Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList)); } @Test void clearHeaders() { - List
    headersToAddList = Arrays.asList(h1, h3); + List headersToAddList = Arrays.asList(h1, h3); clientHeaders.addHeaders(headersToAddList); Assertions.assertEquals(2, clientHeaders.headers().size()); clientHeaders.clearHeaders(); diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilderTest.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilderTest.java index 7be6d672a8..415edf1fd0 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilderTest.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilderTest.java @@ -1,39 +1,29 @@ package org.hl7.fhir.r5.utils.client.network; +import java.util.Collections; import java.util.List; import java.util.Map; import org.hl7.fhir.r5.model.OperationOutcome; +import org.hl7.fhir.utilities.http.HTTPHeaderUtil; +import org.hl7.fhir.utilities.http.HTTPRequest; +import org.hl7.fhir.utilities.http.HTTPHeader; +import org.hl7.fhir.utilities.http.HTTPResult; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import okhttp3.Headers; -import okhttp3.Request; - class FhirRequestBuilderTest { - @Test - @DisplayName("Test default headers are added correctly.") - void addDefaultHeaders() { - Request.Builder request = new Request.Builder().url("http://www.google.com"); - FhirRequestBuilder.addDefaultHeaders(request, null); - - Map> headersMap = request.build().headers().toMultimap(); - Assertions.assertNotNull(headersMap.get("User-Agent"), "User-Agent header null."); - Assertions.assertEquals("hapi-fhir-tooling-client", headersMap.get("User-Agent").get(0), - "User-Agent header not populated with expected value \"hapi-fhir-tooling-client\"."); - } - @Test @DisplayName("Test resource format headers are added correctly (GET).") void addResourceFormatHeadersGET() { String testFormat = "yaml"; - Request.Builder request = new Request.Builder().url("http://www.google.com"); - request.setMethod$okhttp("GET"); - FhirRequestBuilder.addResourceFormatHeaders(request, testFormat); + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.GET); - Map> headersMap = request.build().headers().toMultimap(); + 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 + "."); @@ -45,11 +35,11 @@ void addResourceFormatHeadersGET() { @DisplayName("Test resource format headers are added correctly (POST).") void addResourceFormatHeadersPOST() { String testFormat = "yaml"; - Request.Builder request = new Request.Builder().url("http://www.google.com"); - request.setMethod$okhttp("POST"); - FhirRequestBuilder.addResourceFormatHeaders(request, testFormat); + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com").withMethod(HTTPRequest.HttpMethod.POST); + + Iterable headers = FhirRequestBuilder.getResourceFormatHeaders(request, testFormat); - Map> headersMap = request.build().headers().toMultimap(); + 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 + "."); @@ -59,31 +49,6 @@ void addResourceFormatHeadersPOST() { "Content-Type header not populated with expected value \"" + testFormat + ";charset=" + FhirRequestBuilder.DEFAULT_CHARSET + "\"."); } - @Test - @DisplayName("Test a list of provided headers are added correctly.") - void addHeaders() { - String headerName1 = "headerName1"; - String headerValue1 = "headerValue1"; - String headerName2 = "headerName2"; - String headerValue2 = "headerValue2"; - - Headers headers = new Headers.Builder() - .add(headerName1, headerValue1) - .add(headerName2, headerValue2) - .build(); - - Request.Builder request = new Request.Builder().url("http://www.google.com"); - FhirRequestBuilder.addHeaders(request, headers); - - Map> headersMap = request.build().headers().toMultimap(); - Assertions.assertNotNull(headersMap.get(headerName1), headerName1 + " header null."); - Assertions.assertEquals(headerValue1, headersMap.get(headerName1).get(0), - headerName1 + " header not populated with expected value " + headerValue1 + "."); - Assertions.assertNotNull(headersMap.get(headerName2), headerName2 + " header null."); - Assertions.assertEquals(headerValue2, headersMap.get(headerName2).get(0), - headerName2 + " header not populated with expected value " + headerValue2 + "."); - } - @Test @DisplayName("Test that FATAL issue severity triggers error.") void hasErrorTestFatal() { @@ -120,19 +85,22 @@ void hasErrorTestNoErrors() { @DisplayName("Test that getLocationHeader returns header for 'location'.") void getLocationHeaderWhenOnlyLocationIsSet() { final String expectedLocationHeader = "location_header_value"; - Headers headers = new Headers.Builder() - .add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader) - .build(); - Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); + HTTPResult result = new HTTPResult("source", + 200, + "message", + "contentType", + new byte[0], + List.of(new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader))); + + Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(result.getHeaders())); } @Test @DisplayName("Test that getLocationHeader returns header for 'content-location'.") void getLocationHeaderWhenOnlyContentLocationIsSet() { final String expectedContentLocationHeader = "content_location_header_value"; - Headers headers = new Headers.Builder() - .add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) - .build(); + Iterable headers = List.of(new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader)); + Assertions.assertEquals(expectedContentLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); } @@ -141,18 +109,18 @@ void getLocationHeaderWhenOnlyContentLocationIsSet() { void getLocationHeaderWhenLocationAndContentLocationAreSet() { final String expectedLocationHeader = "location_header_value"; final String expectedContentLocationHeader = "content_location_header_value"; - Headers headers = new Headers.Builder() - .add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader) - .add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) - .build(); + + Iterable headers = List.of( + new HTTPHeader(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader), + new HTTPHeader(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) + ); + Assertions.assertEquals(expectedLocationHeader, FhirRequestBuilder.getLocationHeader(headers)); } @Test @DisplayName("Test that getLocationHeader returns null when no location available.") void getLocationHeaderWhenNoLocationSet() { - Headers headers = new Headers.Builder() - .build(); - Assertions.assertNull(FhirRequestBuilder.getLocationHeader(headers)); + Assertions.assertNull(FhirRequestBuilder.getLocationHeader(Collections.emptyList())); } } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeader.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeader.java new file mode 100644 index 0000000000..b45e9ff7fd --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeader.java @@ -0,0 +1,21 @@ +package org.hl7.fhir.utilities.http; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.With; + +import javax.annotation.Nonnull; + +@EqualsAndHashCode +public class HTTPHeader { + @With @Getter @Nonnull + private final String name; + @With @Getter + private final String value; + + public HTTPHeader(@Nonnull String name, String value) { + this.name = name; + this.value = value; + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeaderUtil.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeaderUtil.java new file mode 100644 index 0000000000..06bf482424 --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPHeaderUtil.java @@ -0,0 +1,45 @@ +package org.hl7.fhir.utilities.http; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HTTPHeaderUtil { + + public static final String USER_AGENT = "User-Agent"; + + + public static Map> getMultimap(Iterable headers) { + Map> result = new HashMap<>(); + if (headers != null) { + for (HTTPHeader header : headers) { + List values = result.getOrDefault(header.getName(), new ArrayList<>()); + values.add(header.getValue()); + result.put(header.getName(), values); + } + } + return result; + } + + public static Iterable getHeaders(Iterable headers, String key) { + List result = new ArrayList<>(); + if (headers != null) { + for (HTTPHeader header : headers) { + if (header.getName().equalsIgnoreCase(key)) { + result.add(header.getValue()); + } + } + } + return result; + } + + public static String getSingleHeader(Iterable headers, String key) { + for (HTTPHeader header : headers) { + if (header.getName().equalsIgnoreCase(key)) { + return header.getValue(); + } + } + return null; + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPRequest.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPRequest.java new file mode 100644 index 0000000000..19bfa9d364 --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPRequest.java @@ -0,0 +1,54 @@ +package org.hl7.fhir.utilities.http; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.With; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; + +@AllArgsConstructor +public class HTTPRequest { + + public HTTPRequest() { + url = null; + method = HttpMethod.GET; + body = null; + contentType = null; + headers = Collections.emptyList(); + } + + public enum HttpMethod { + GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH + } + + @Getter @Nullable + private final URL url; + + public HTTPRequest withUrl(URL url) { + return new HTTPRequest(url, method, body, contentType, headers); + } + + public HTTPRequest withUrl(String url) { + try { + return withUrl(new URL(url)); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid URL: " + url, e); + } + } + + @With @Getter + private final HttpMethod method; + + @With @Getter @Nullable + private final byte[] body; + + @With @Getter @Nullable + private final String contentType; + + @With @Getter @Nonnull + private final Iterable headers; +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java index 0a3986ec1c..3946442b83 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java @@ -2,39 +2,40 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.*; +import lombok.Getter; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; public class HTTPResult { - private int code; - private String contentType; - private byte[] content; - private String source; - private String message; - - + + @Getter + private final int code; + @Getter + private final String contentType; + @Getter + private final byte[] content; + @Getter + private final String source; + @Getter + private final String message; + @Getter + private final Iterable headers; + public HTTPResult(String source, int code, String message, String contentType, byte[] content) { + this(source, code, message, contentType, content, Collections.emptyList()); + } + + + public HTTPResult(String source, int code, String message, String contentType, byte[] content, Iterable headers) { super(); this.source = source; this.code = code; this.contentType = contentType; this.content = content; this.message = message; - } - - public int getCode() { - return code; - } - public String getContentType() { - return contentType; - } - public byte[] getContent() { - return content; - } - - public String getSource() { - return source; + this.headers = headers; } public void checkThrowException() throws IOException { @@ -52,11 +53,7 @@ public void checkThrowException() throws IOException { } } - public String getMessage() { - return message; - } - public String getContentAsString() { return new String(content, StandardCharsets.UTF_8); - } + } } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java new file mode 100644 index 0000000000..b12c841757 --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java @@ -0,0 +1,170 @@ +package org.hl7.fhir.utilities.http; + +import okhttp3.*; +import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.http.okhttpimpl.LoggingInterceptor; +import org.hl7.fhir.utilities.http.okhttpimpl.ProxyAuthenticator; +import org.hl7.fhir.utilities.http.okhttpimpl.RetryInterceptor; +import org.hl7.fhir.utilities.settings.ServerDetailsPOJO; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ManagedFhirWebAccessor extends ManagedWebAccessorBase { + + /** + * The singleton instance of the HttpClient, used for all requests. + */ + private static OkHttpClient okHttpClient; + + private long timeout; + private TimeUnit timeoutUnit; + private int retries; + private ToolingClientLogger logger; + private LoggingInterceptor loggingInterceptor; + + public ManagedFhirWebAccessor withTimeout(long timeout, TimeUnit timeoutUnit) { + this.timeout = timeout; + this.timeoutUnit = timeoutUnit; + return this; + } + + public ManagedFhirWebAccessor withRetries(int retries) { + this.retries = retries; + return this; + } + + public ManagedFhirWebAccessor withLogger(ToolingClientLogger logger) { + this.logger = logger; + this.loggingInterceptor = new LoggingInterceptor(logger); + return this; + } + + + public ManagedFhirWebAccessor(String userAgent, List serverAuthDetails) { + super(userAgent, serverAuthDetails); + this.timeout = 5000; + this.timeoutUnit = TimeUnit.MILLISECONDS; + } + + protected HTTPRequest httpRequestWithDefaultHeaders(HTTPRequest request) { + List headers = new ArrayList<>(); + if (HTTPHeaderUtil.getSingleHeader(request.getHeaders(), HTTPHeaderUtil.USER_AGENT) == null + && getUserAgent() != null) { + headers.add(new HTTPHeader(HTTPHeaderUtil.USER_AGENT, getUserAgent())); + } + request.getHeaders().forEach(headers::add); + return request.withHeaders(headers); + } + + protected HTTPRequest requestWithManagedHeaders(HTTPRequest httpRequest) { + HTTPRequest requestWithDefaultHeaders = httpRequestWithDefaultHeaders(httpRequest); + + List headers = new ArrayList<>(); + requestWithDefaultHeaders.getHeaders().forEach(headers::add); + + for (Map.Entry entry : this.getHeaders().entrySet()) { + headers.add(new HTTPHeader(entry.getKey(), entry.getValue())); + } + + if (getAuthenticationMode() != null) { + if (getAuthenticationMode() != HTTPAuthenticationMode.NONE) { + switch (getAuthenticationMode()) { + case BASIC: + final String basicCredential = Credentials.basic(getUsername(), getPassword()); + headers.add(new HTTPHeader("Authorization", basicCredential)); + break; + case TOKEN: + String tokenCredential = "Bearer " + getToken(); + headers.add(new HTTPHeader("Authorization", tokenCredential)); + break; + case APIKEY: + String apiKeyCredential = getToken(); + headers.add(new HTTPHeader("Api-Key", apiKeyCredential)); + break; + } + } + } else { + ServerDetailsPOJO settings = ManagedWebAccessUtils.getServer(httpRequest.getUrl().toString(), getServerAuthDetails()); + if (settings != null) { + switch (settings.getAuthenticationType()) { + case "basic": + final String basicCredential = Credentials.basic(settings.getUsername(), settings.getPassword()); + headers.add(new HTTPHeader("Authorization", basicCredential)); + break; + case "token": + String tokenCredential = "Bearer " + settings.getToken(); + headers.add(new HTTPHeader("Authorization", tokenCredential)); + break; + case "apikey": + String apiKeyCredential = settings.getApikey(); + headers.add(new HTTPHeader("Api-Key", apiKeyCredential)); + break; + } + } + } + return httpRequest.withHeaders(headers); + } + + public HTTPResult httpCall(HTTPRequest httpRequest) throws IOException { + switch (ManagedWebAccess.getAccessPolicy()) { + case DIRECT: + + HTTPRequest httpRequestWithDirectHeaders = requestWithManagedHeaders(httpRequest); + assert httpRequestWithDirectHeaders.getUrl() != null; + + RequestBody body = httpRequestWithDirectHeaders.getBody() == null ? null : RequestBody.create(httpRequestWithDirectHeaders.getBody()); + Request.Builder requestBuilder = new Request.Builder() + .url(httpRequestWithDirectHeaders.getUrl()) + .method(httpRequestWithDirectHeaders.getMethod().name(), body); + + for (HTTPHeader header : httpRequestWithDirectHeaders.getHeaders()) { + requestBuilder.addHeader(header.getName(), header.getValue()); + } + OkHttpClient okHttpClient = getOkHttpClient(); + //TODO check and throw based on httpRequest: + + if (!ManagedWebAccess.inAllowedPaths(httpRequestWithDirectHeaders.getUrl().toString())) { + throw new IOException("The pathname '"+httpRequestWithDirectHeaders.getUrl().toString()+"' cannot be accessed by policy");} + Response response = okHttpClient.newCall(requestBuilder.build()).execute(); + return getHTTPResult(response); + case MANAGED: + HTTPRequest httpRequestWithManagedHeaders = requestWithManagedHeaders(httpRequest); + assert httpRequestWithManagedHeaders.getUrl() != null; + return ManagedWebAccess.getFhirWebAccessor().httpCall(httpRequestWithManagedHeaders); + case PROHIBITED: + throw new IOException("Access to the internet is not allowed by local security policy"); + default: + throw new IOException("Internal Error"); + } + } + + private HTTPResult getHTTPResult(Response execute) throws IOException { + return new HTTPResult(execute.request().url().toString(), execute.code(), execute.message(), execute.header("Content-Type"), execute.body() != null && execute.body().contentLength() != 0 ? execute.body().bytes() : null, getHeadersFromResponse(execute)); + } + + private Iterable getHeadersFromResponse(Response response) { + List headers = new ArrayList<>(); + for (String name : response.headers().names()) { + headers.add(new HTTPHeader(name, response.header(name))); + } + return headers; + } + + private OkHttpClient getOkHttpClient() { + if (okHttpClient == null) { + okHttpClient = new OkHttpClient(); + } + OkHttpClient.Builder builder = okHttpClient.newBuilder(); + if (logger != null) builder.addInterceptor(loggingInterceptor); + builder.addInterceptor(new RetryInterceptor(retries)); + builder.proxyAuthenticator(new ProxyAuthenticator()); + return builder.connectTimeout(timeout, timeoutUnit) + .writeTimeout(timeout, timeoutUnit) + .readTimeout(timeout, timeoutUnit).build(); + } + +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccess.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccess.java index f1afac25db..3ebb85c20d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccess.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccess.java @@ -31,23 +31,16 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; -import org.hl7.fhir.utilities.Utilities; +import lombok.Getter; +import org.hl7.fhir.utilities.settings.FhirSettings; import org.hl7.fhir.utilities.settings.ServerDetailsPOJO; /** - * see security.md - manages access to the local file system by the FHIR HAPI Core library - * + * see security.md - manages web access by the FHIR HAPI Core library + *

    * By using accessPolicy, allowedDomains and accessor, a host java application can control * whether this library has direct access to the web (and which domains it is allowed to access), * or whether the host application provides controlled access, or whether no access is allowed at all @@ -57,13 +50,17 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS * */ public class ManagedWebAccess { - + public interface IWebAccessor { HTTPResult get(String url, String accept, Map headers) throws IOException; HTTPResult post(String url, byte[] bytes, String contentType, String accept, Map headers) throws IOException; HTTPResult put(String url, byte[] bytes, String contentType, String accept, Map headers) throws IOException; } + public interface IFhirWebAccessor { + HTTPResult httpCall(HTTPRequest httpRequest); + } + public enum WebAccessPolicy { DIRECT, // open access to the web, though access can be restricted only to domains in AllowedDomains MANAGED, // no access except by the IWebAccessor @@ -71,12 +68,18 @@ public enum WebAccessPolicy { } private static WebAccessPolicy accessPolicy = WebAccessPolicy.DIRECT; // for legacy reasons + //TODO get this from fhir settings private static List allowedDomains = new ArrayList<>(); + @Getter private static IWebAccessor accessor; + + @Getter + private static IFhirWebAccessor fhirWebAccessor; + + @Getter private static String userAgent; private static List serverAuthDetails; - - + public static WebAccessPolicy getAccessPolicy() { return accessPolicy; } @@ -97,37 +100,51 @@ static boolean inAllowedPaths(String pathname) { return false; } - public static String getUserAgent() { - return userAgent; - } - public static void setUserAgent(String userAgent) { ManagedWebAccess.userAgent = userAgent; } - public static IWebAccessor getAccessor() { - return accessor; + public static ManagedWebAccessor accessor() { + return new ManagedWebAccessor(userAgent, serverAuthDetails); } - public static ManagedWebAccessBuilder builder() { - return new ManagedWebAccessBuilder(userAgent, serverAuthDetails); + public static ManagedFhirWebAccessor fhirAccessor() { + return new ManagedFhirWebAccessor(userAgent, serverAuthDetails); } public static HTTPResult get(String url) throws IOException { - return builder().get(url); + return accessor().get(url); } public static HTTPResult get(String url, String accept) throws IOException { - return builder().withAccept(accept).get(url); + return accessor().get(url, accept); } public static HTTPResult post(String url, byte[] content, String contentType, String accept) throws IOException { - return builder().withAccept(accept).post(url, content, contentType); + return accessor().post(url, content, contentType, accept); } - public static HTTPResult put(String url, byte[] content, String contentType, String accept) throws IOException { - return builder().withAccept(accept).put(url, content, contentType); + return accessor().put(url, content, contentType, accept); + } + + public static HTTPResult httpCall(HTTPRequest httpRequest) throws IOException { + return fhirAccessor().httpCall(httpRequest); + } + + public static void loadFromFHIRSettings() { + setAccessPolicy(FhirSettings.isProhibitNetworkAccess() ? WebAccessPolicy.PROHIBITED : WebAccessPolicy.DIRECT); + setUserAgent("hapi-fhir-tooling-client"); + serverAuthDetails = new ArrayList<>(); + serverAuthDetails.addAll(FhirSettings.getPackageServers()); + serverAuthDetails.addAll(FhirSettings.getTerminologyServers()); } + public static void loadFromFHIRSettings(FhirSettings settings) { + setAccessPolicy(settings.isProhibitNetworkAccess() ? WebAccessPolicy.PROHIBITED : WebAccessPolicy.DIRECT); + setUserAgent("hapi-fhir-tooling-client"); + serverAuthDetails = new ArrayList<>(); + serverAuthDetails.addAll(settings.getPackageServers()); + serverAuthDetails.addAll(settings.getTerminologyServers()); + } } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessUtils.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessUtils.java new file mode 100644 index 0000000000..a973536335 --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessUtils.java @@ -0,0 +1,17 @@ +package org.hl7.fhir.utilities.http; + +import org.hl7.fhir.utilities.settings.ServerDetailsPOJO; + +public class ManagedWebAccessUtils { + + public static ServerDetailsPOJO getServer(String url, Iterable serverAuthDetails) { + if (serverAuthDetails != null) { + for (ServerDetailsPOJO t : serverAuthDetails) { + if (url.startsWith(t.getUrl())) { + return t; + } + } + } + return null; + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java similarity index 54% rename from org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessBuilder.java rename to org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java index 79f27add70..4c364a2f59 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessBuilder.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java @@ -9,61 +9,30 @@ import org.hl7.fhir.utilities.settings.ServerDetailsPOJO; +/** + * Simple HTTP client for making requests to a server. + */ +public class ManagedWebAccessor extends ManagedWebAccessorBase { -public class ManagedWebAccessBuilder { - - private String userAgent; - private HTTPAuthenticationMode authenticationMode; - private String username; - private String password; - private String token; - private String accept; - private List serverAuthDetails; - private Map headers = new HashMap(); - - public ManagedWebAccessBuilder(String userAgent, List serverAuthDetails) { - this.userAgent = userAgent; - this.serverAuthDetails = serverAuthDetails; - } - - public ManagedWebAccessBuilder withAccept(String accept) { - this.accept = accept; - return this; - } - - public ManagedWebAccessBuilder withHeader(String name, String value) { - headers.put(name, value); - return this; - } - - public ManagedWebAccessBuilder withBasicAuth(String username, String password) { - this.authenticationMode = HTTPAuthenticationMode.BASIC; - this.username = username; - this.password = password; - return this; - } - - public ManagedWebAccessBuilder withToken(String token) { - this.authenticationMode = HTTPAuthenticationMode.TOKEN; - this.token = token; - return this; + public ManagedWebAccessor(String userAgent, List serverAuthDetails) { + super(userAgent, serverAuthDetails); } - private Map headers() { - Map headers = new HashMap(); - headers.putAll(this.headers); - if (authenticationMode == HTTPAuthenticationMode.TOKEN) { - headers.put("Authorization", "Bearer " + token); - } else if (authenticationMode == HTTPAuthenticationMode.BASIC) { - String auth = username+":"+password; + private Map newHeaders() { + Map headers = new HashMap(this.getHeaders()); + if (getAuthenticationMode() == HTTPAuthenticationMode.TOKEN) { + headers.put("Authorization", "Bearer " + getToken()); + } else if (getAuthenticationMode() == HTTPAuthenticationMode.BASIC) { + String auth = getUsername() +":"+ getPassword(); byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8)); headers.put("Authorization", "Basic " + new String(encodedAuth)); + } else if (getAuthenticationMode() == HTTPAuthenticationMode.APIKEY) { + headers.put("Api-Key", getToken()); } - if (userAgent != null) { - headers.put("User-Agent", userAgent); + if (getUserAgent() != null) { + headers.put("User-Agent", getUserAgent()); } - return headers; } @@ -72,25 +41,32 @@ private SimpleHTTPClient setupClient(String url) throws IOException { throw new IOException("The pathname '"+url+"' cannot be accessed by policy"); } SimpleHTTPClient client = new SimpleHTTPClient(); - if (userAgent != null) { - client.addHeader("User-Agent", userAgent); + + for (Map.Entry entry : this.getHeaders().entrySet()) { + client.addHeader(entry.getKey(), entry.getValue()); + } + + if (getUserAgent() != null) { + client.addHeader("User-Agent", getUserAgent()); } - if (authenticationMode != null && authenticationMode != HTTPAuthenticationMode.NONE) { - client.setAuthenticationMode(authenticationMode); - switch (authenticationMode) { + if (getAuthenticationMode() != null) { + if (getAuthenticationMode() != HTTPAuthenticationMode.NONE) { + client.setAuthenticationMode(getAuthenticationMode()); + switch (getAuthenticationMode()) { case BASIC : - client.setUsername(username); - client.setPassword(password); + client.setUsername(getUsername()); + client.setPassword(getPassword()); break; case TOKEN : - client.setToken(token); + client.setToken(getToken()); break; case APIKEY : - client.setApiKey(token); + client.setApiKey(getToken()); break; } + } } else { - ServerDetailsPOJO settings = getServer(url); + ServerDetailsPOJO settings = ManagedWebAccessUtils.getServer(url, getServerAuthDetails()); if (settings != null) { switch (settings.getAuthenticationType()) { case "basic" : @@ -109,32 +85,23 @@ private SimpleHTTPClient setupClient(String url) throws IOException { } } } - if (username != null || token != null) { - - client.setAuthenticationMode(authenticationMode); + if (getUsername() != null || getToken() != null) { + client.setAuthenticationMode(getAuthenticationMode()); } return client; } - - private ServerDetailsPOJO getServer(String url) { - if (serverAuthDetails != null) { - for (ServerDetailsPOJO t : serverAuthDetails) { - if (url.startsWith(t.getUrl())) { - return t; - } - } - } - return null; + public HTTPResult get(String url) throws IOException { + return get(url, null); } - public HTTPResult get(String url) throws IOException { + public HTTPResult get(String url, String accept) throws IOException { switch (ManagedWebAccess.getAccessPolicy()) { case DIRECT: SimpleHTTPClient client = setupClient(url); return client.get(url, accept); case MANAGED: - return ManagedWebAccess.getAccessor().get(url, accept, headers()); + return ManagedWebAccess.getAccessor().get(url, accept, newHeaders()); case PROHIBITED: throw new IOException("Access to the internet is not allowed by local security policy"); default: @@ -142,14 +109,17 @@ public HTTPResult get(String url) throws IOException { } } - public HTTPResult post(String url, byte[] content, String contentType) throws IOException { + return post(url, content, contentType, null); + } + + public HTTPResult post(String url, byte[] content, String contentType, String accept) throws IOException { switch (ManagedWebAccess.getAccessPolicy()) { case DIRECT: SimpleHTTPClient client = setupClient(url); return client.post(url, contentType, content, accept); case MANAGED: - return ManagedWebAccess.getAccessor().post(url, content, contentType, accept, headers()); + return ManagedWebAccess.getAccessor().post(url, content, contentType, accept, newHeaders()); case PROHIBITED: throw new IOException("Access to the internet is not allowed by local security policy"); default: @@ -158,17 +128,20 @@ public HTTPResult post(String url, byte[] content, String contentType) throws IO } public HTTPResult put(String url, byte[] content, String contentType) throws IOException { + return put(url, content, contentType, null); + } + + public HTTPResult put(String url, byte[] content, String contentType, String accept) throws IOException { switch (ManagedWebAccess.getAccessPolicy()) { case DIRECT: SimpleHTTPClient client = setupClient(url); return client.put(url, contentType, content, accept); case MANAGED: - return ManagedWebAccess.getAccessor().put(url, content, contentType, accept, headers()); + return ManagedWebAccess.getAccessor().put(url, content, contentType, accept, newHeaders()); case PROHIBITED: throw new IOException("Access to the internet is not allowed by local security policy"); default: throw new IOException("Internal Error"); } } - } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessorBase.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessorBase.java new file mode 100644 index 0000000000..67e4df641d --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessorBase.java @@ -0,0 +1,78 @@ +package org.hl7.fhir.utilities.http; + +import lombok.Getter; +import org.hl7.fhir.utilities.settings.ServerDetailsPOJO; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class ManagedWebAccessorBase> { + @Getter + private final String userAgent; + @Getter + private HTTPAuthenticationMode authenticationMode; + @Getter + private String username; + @Getter + private String password; + @Getter + private String token; + + @Getter + private final List serverAuthDetails; + @Getter + private final Map headers = new HashMap<>(); + + public ManagedWebAccessorBase(String userAgent, List serverAuthDetails) { + this.userAgent = userAgent; + this.serverAuthDetails = serverAuthDetails; + } + + @SuppressWarnings("unchecked") + final B self() { + return (B) this; + } + + public B withHeader(String name, String value) { + headers.put(name, value); + return self(); + } + + public B withBasicAuth(String username, String password) { + this.authenticationMode = HTTPAuthenticationMode.BASIC; + this.username = username; + this.password = password; + return self(); + } + + public B withToken(String token) { + this.authenticationMode = HTTPAuthenticationMode.TOKEN; + this.token = token; + return self(); + } + + public B withApiKey(String apiKey) { + this.authenticationMode = HTTPAuthenticationMode.APIKEY; + this.token = apiKey; + return self(); + } + + public B withNoneAuth() { + this.authenticationMode = HTTPAuthenticationMode.NONE; + setAllAuthHeadersToNull(); + return self(); + } + + public B withServerSpecificAuth() { + this.authenticationMode = null; + setAllAuthHeadersToNull(); + return self(); + } + + private void setAllAuthHeadersToNull() { + this.token = null; + this.username = null; + this.password = null; + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java index 0e448293b2..bf35ee9061 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java @@ -20,27 +20,10 @@ public class SimpleHTTPClient { - - public static class Header { - private String name; - private String value; - public Header(String name, String value) { - super(); - this.name = name; - this.value = value; - } - public String getName() { - return name; - } - public String getValue() { - return value; - } - } - private static final int MAX_REDIRECTS = 5; private static int counter = 1; - private List

    headers = new ArrayList<>(); + private List headers = new ArrayList<>(); @Getter @Setter private HTTPAuthenticationMode authenticationMode; @@ -58,7 +41,7 @@ public String getValue() { private String apiKey; public void addHeader(String name, String value) { - headers.add(new Header(name, value)); + headers.add(new HTTPHeader(name, value)); } public HTTPResult get(String url) throws IOException { @@ -114,8 +97,8 @@ public HTTPResult get(String url, String accept) throws IOException { private void setHeaders(HttpURLConnection c) { if (headers != null) { - for (Header h : headers) { - c.setRequestProperty(h.getName(), h.getValue()); + for (HTTPHeader header : headers) { + c.setRequestProperty(header.getName(), header.getValue()); } } c.setConnectTimeout(15000); @@ -126,7 +109,7 @@ private void setHeaders(HttpURLConnection c) { private void setAuthenticationHeader(HttpURLConnection c) { String authHeaderValue = null; if (authenticationMode == HTTPAuthenticationMode.TOKEN) { - authHeaderValue = "Bearer " + new String(token); + authHeaderValue = "Bearer " + token; } else if (authenticationMode == HTTPAuthenticationMode.BASIC) { String auth = username+":"+password; byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8)); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/LoggingInterceptor.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/LoggingInterceptor.java new file mode 100644 index 0000000000..35dd0f9470 --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/LoggingInterceptor.java @@ -0,0 +1,69 @@ +package org.hl7.fhir.utilities.http.okhttpimpl; + +import okhttp3.*; +import okio.Buffer; +import org.hl7.fhir.utilities.ToolingClientLogger; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class LoggingInterceptor implements Interceptor{ + + private ToolingClientLogger logger; + + public LoggingInterceptor(ToolingClientLogger logger) { + this.logger = logger; + } + + public LoggingInterceptor setLogger(ToolingClientLogger logger) { + this.logger = logger; + return this; + } + + @Override + public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException { + // Log Request + Request request = chain.request(); + List hdrs = new ArrayList<>(); + for (String s : request.headers().toString().split("\\n")) { + hdrs.add(s.trim()); + } + byte[] cnt = null; + if (request.body() != null) { + Buffer buf = new Buffer(); + request.body().writeTo(buf); + cnt = buf.readByteArray(); + } + if (logger != null) { + logger.logRequest(request.method(), request.url().toString(), hdrs, cnt); + } + + // Log Response + Response response = null; + response = chain.proceed(chain.request()); + + MediaType contentType = null; + byte[] bodyBytes = null; + if (response.body() != null) { + contentType = response.body().contentType(); + bodyBytes = response.body().bytes(); + } + + // Get Headers as List + List headerList = new ArrayList<>(); + Map> headerMap = response.headers().toMultimap(); + headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value))); + + if (logger != null) { + long responseTimeInMillis = response.receivedResponseAtMillis() - response.sentRequestAtMillis(); + logger.logResponse(Integer.toString(response.code()), headerList, bodyBytes, responseTimeInMillis); + } + + // Reading byte[] clears body. Need to recreate. + ResponseBody body = ResponseBody.create(bodyBytes, contentType); + return response.newBuilder().body(body).build(); + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/ProxyAuthenticator.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/ProxyAuthenticator.java new file mode 100644 index 0000000000..1f59167d3c --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/ProxyAuthenticator.java @@ -0,0 +1,24 @@ +package org.hl7.fhir.utilities.http.okhttpimpl; + +import okhttp3.*; + +import java.io.IOException; + +public class ProxyAuthenticator implements Authenticator { + protected static final String HTTP_PROXY_USER = "http.proxyUser"; + protected static final String HTTP_PROXY_PASS = "http.proxyPassword"; + protected static final String HEADER_PROXY_AUTH = "Proxy-Authorization"; + + @Override + public Request authenticate(Route route, Response response) throws IOException { + final String httpProxyUser = System.getProperty(HTTP_PROXY_USER); + final String httpProxyPass = System.getProperty(HTTP_PROXY_PASS); + if (httpProxyUser != null && httpProxyPass != null) { + String credential = Credentials.basic(httpProxyUser, httpProxyPass); + return response.request().newBuilder() + .header(HEADER_PROXY_AUTH, credential) + .build(); + } + return response.request().newBuilder().build(); + } +} diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/RetryInterceptor.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/RetryInterceptor.java similarity index 94% rename from org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/RetryInterceptor.java rename to org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/RetryInterceptor.java index e3b6ec084e..ca71c9a019 100644 --- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/utils/client/network/RetryInterceptor.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/okhttpimpl/RetryInterceptor.java @@ -1,11 +1,11 @@ -package org.hl7.fhir.dstu3.utils.client.network; - -import java.io.IOException; +package org.hl7.fhir.utilities.http.okhttpimpl; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; +import java.io.IOException; + /** * An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the number of times we retry a to execute a * given request, before reporting a failure. This includes unsuccessful return codes and timeouts. @@ -26,7 +26,7 @@ public RetryInterceptor(int maxRetry) { } @Override - public Response intercept(Interceptor.Chain chain) throws IOException { + public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = null; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 0d4b2a79d6..e5c0296078 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -178,7 +178,6 @@ public class I18nConstants { public static final String DIFFERENTIAL_WALKS_INTO____BUT_THE_BASE_DOES_NOT_AND_THERE_IS_NOT_A_SINGLE_FIXED_TYPE_THE_TYPE_IS__THIS_IS_NOT_HANDLED_YET = "Differential_walks_into____but_the_base_does_not_and_there_is_not_a_single_fixed_type_The_type_is__This_is_not_handled_yet"; public static final String DISCRIMINATOR_BAD_PATH = "DISCRIMINATOR_BAD_PATH"; public static final String DISCRIMINATOR__IS_BASED_ON_ELEMENT_EXISTENCE_BUT_SLICE__NEITHER_SETS_MIN1_OR_MAX0 = "Discriminator__is_based_on_element_existence_but_slice__neither_sets_min1_or_max0"; - public static final String DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_MULTIPLE_TYPES = "Discriminator__is_based_on_type_but_slice__in__has_multiple_types"; public static final String DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_NO_TYPES = "Discriminator__is_based_on_type_but_slice__in__has_no_types"; public static final String DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF = "Display_Name_for__should_be_one_of__instead_of"; public static final String DISPLAY_NAME_WS_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF = "Display_Name_WS_for__should_be_one_of__instead_of"; @@ -475,7 +474,6 @@ public class I18nConstants { public static final String PROBLEM_PROCESSING_EXPRESSION__IN_PROFILE__PATH__ = "Problem_processing_expression__in_profile__path__"; public static final String PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE_WITH_A_PROFILE__IN_PROFILE_ = "Profile_based_discriminators_must_have_a_type_with_a_profile__in_profile_"; public static final String PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE__IN_PROFILE_ = "Profile_based_discriminators_must_have_a_type__in_profile_"; - public static final String PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_ONLY_ONE_TYPE__IN_PROFILE = "Profile_based_discriminators_must_have_only_one_type__in_profile"; public static final String PROFILE_EXT_NOT_HERE = "Profile_EXT_Not_Here"; public static final String PROFILE_VAL_MISSINGELEMENT = "Profile_VAL_MissingElement"; public static final String PROFILE_VAL_NOTALLOWED = "Profile_VAL_NotAllowed"; @@ -1122,4 +1120,9 @@ public class I18nConstants { public static final String CODESYSTEM_CS_COMPLETE_AND_EMPTY = "CODESYSTEM_CS_COMPLETE_AND_EMPTY"; public static final String VALIDATION_VAL_VERSION_NOHASH = "VALIDATION_VAL_VERSION_NOHASH"; public static final String PRIMITIVE_TOO_SHORT = "PRIMITIVE_TOO_SHORT"; + public static final String CANONICAL_MULTIPLE_VERSIONS_KNOWN = "CANONICAL_MULTIPLE_VERSIONS_KNOWN"; + public static final String SD_PATH_NO_SLICING = "SD_PATH_NO_SLICING"; + public static final String SD_PATH_SLICING_DEPRECATED = "SD_PATH_SLICING_DEPRECATED"; + public static final String SD_PATH_NOT_VALID = "SD_PATH_NOT_VALID"; + public static final String SD_PATH_ERROR = "SD_PATH_ERROR"; } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java index 4bf8273560..6f6feeb849 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.Set; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; @@ -24,7 +23,7 @@ import org.hl7.fhir.utilities.http.HTTPAuthenticationMode; import org.hl7.fhir.utilities.http.HTTPResult; import org.hl7.fhir.utilities.http.ManagedWebAccess; -import org.hl7.fhir.utilities.http.ManagedWebAccessBuilder; +import org.hl7.fhir.utilities.http.ManagedWebAccessor; import org.hl7.fhir.utilities.json.model.JsonArray; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.model.JsonProperty; @@ -176,13 +175,13 @@ public Date getNewPackages(Date lastCalled, List updates) { } private InputStream fetchUrl(String source, String accept) throws IOException { - ManagedWebAccessBuilder client = ManagedWebAccess.builder().withAccept(accept); + ManagedWebAccessor webAccessor = ManagedWebAccess.accessor(); if (server.getAuthenticationMode() == HTTPAuthenticationMode.TOKEN) { - client.withToken(server.getToken()); + webAccessor.withToken(server.getToken()); } else if (server.getAuthenticationMode() == HTTPAuthenticationMode.BASIC) { - client.withBasicAuth(server.getUsername(), server.getPassword()); + webAccessor.withBasicAuth(server.getUsername(), server.getPassword()); } - HTTPResult res = client.get(source); + HTTPResult res = webAccessor.get(source, accept); res.checkThrowException(); return new ByteArrayInputStream(res.getContent()); } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/FhirSettings.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/FhirSettings.java index dc69f3fd04..060ce2195d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/FhirSettings.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/FhirSettings.java @@ -4,6 +4,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -232,6 +233,14 @@ public static List getPackageServers() { if (instance.fhirSettings.getPackageManagement() == null) { return Collections.emptyList(); } - return List.of(instance.fhirSettings.getPackageManagement().getServers().toArray(new ServerDetailsPOJO[]{})); + return Arrays.asList(instance.fhirSettings.getPackageManagement().getServers().toArray(new ServerDetailsPOJO[]{})); + } + + public static List getTerminologyServers() { + getInstance(); + if (instance.fhirSettings.getTerminologyServers() == null) { + return Collections.emptyList(); + } + return Arrays.asList(instance.fhirSettings.getTerminologyServers().getServers().toArray(new ServerDetailsPOJO[]{})); } } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 1f44ca339c..b4c87aeaa5 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -179,8 +179,6 @@ Did_not_find_type_root_ = Did not find type root: {0} Differential_does_not_have_a_slice__b_of_____in_profile_ = Differential in profile {5} does not have a slice at {6} (on {0}, position {1} of {2} / {3} / {4}) Differential_walks_into____but_the_base_does_not_and_there_is_not_a_single_fixed_type_The_type_is__This_is_not_handled_yet = Differential walks into ''{0} (@ {1})'', but the base does not, and there is not a single fixed type. The type is {2}. This is not handled yet Discriminator__is_based_on_element_existence_but_slice__neither_sets_min1_or_max0 = Discriminator ({0}) is based on element existence, but slice {1} neither sets min>=1 or max=0 -Discriminator__is_based_on_type_but_slice__in__has_multiple_types_one = -Discriminator__is_based_on_type_but_slice__in__has_multiple_types_other = Discriminator ({1}) is based on type, but slice {2} in {3} has {0} types: {4} Discriminator__is_based_on_type_but_slice__in__has_no_types = Discriminator ({0}) is based on type, but slice {1} in {2} has no types Display_Name_WS_for__should_be_one_of__instead_of_one = Wrong whitespace in Display Name ''{4}'' for {1}#{2}. Valid display is {3} (for the language(s) ''{5}'') Display_Name_WS_for__should_be_one_of__instead_of_other = Wrong whitespace in Display Name ''{4}'' for {1}#{2}. Valid display is one of {0} choices: {3} (for the language(s) ''{5}'') @@ -486,8 +484,6 @@ Profile___has_no_base_and_no_snapshot = Profile {0} ({1}) has no base and no sna Profile__does_not_match_for__because_of_the_following_profile_issues__ = Profile {0} does not match for {1} because of the following profile issues: {2} Profile_based_discriminators_must_have_a_type__in_profile_ = Profile based discriminators must have a type ({0} in profile {1}) Profile_based_discriminators_must_have_a_type_with_a_profile__in_profile_ = Profile based discriminators must have a type with a profile ({0} in profile {1}) -Profile_based_discriminators_must_have_only_one_type__in_profile_one = -Profile_based_discriminators_must_have_only_one_type__in_profile_other = Profile based discriminators must have only one type ({1} in profile {2}) but found {0} types QUESTIONNAIRE_QR_ITEM_BADOPTION_CS = The code provided {1} cannot be validated in the options value set ({2}) in the questionnaire because the system {0} could not be found QUESTIONNAIRE_Q_DERIVATION_TYPE_IGNORED = The derivation type ''{0}'' means that no derivation checking has been performed against this questionnaire QUESTIONNAIRE_Q_DERIVATION_TYPE_UNKNOWN = The derivation type ''{0}'' is unknown, which means that no derivation checking has been performed against this questionnaire @@ -1090,7 +1086,7 @@ XHTML_XHTML_NS_InValid = Wrong namespace on the XHTML (''{0}'', should be ''{1}' XHTML_XHTML_Name_Invalid = Wrong name on the XHTML (''{0}'') - must start with div XSI_TYPE_UNNECESSARY = xsi:type is unnecessary at this point XSI_TYPE_WRONG = The xsi:type value ''{0}'' is wrong (should be ''{1}''). Note that xsi:type is unnecessary at this point -_DT_Fixed_Wrong = Value is ''{0}'' but must be ''{1}''{2} +_DT_Fixed_Wrong = Value is ''{0}'' but is fixed to ''{1}'' in the profile {2} _has_children__and_multiple_types__in_profile_ = {0} has children ({1}) and multiple types ({2}) in profile {3} _has_children__for_type__in_profile__but_cant_find_type = {0} has children ({1}) for type {2} in profile {3}, but can''t find type _has_no_children__and_no_types_in_profile_ = {0} has no children ({1}) and no types in profile {2} @@ -1155,4 +1151,8 @@ SD_ED_ADDITIONAL_BINDING_USAGE_INVALID_TYPE = The Usage Context value must be of CODESYSTEM_CS_COMPLETE_AND_EMPTY = When a CodeSystem has content = ''complete'', it doesn't make sense for there to be no concepts defined VALIDATION_VAL_VERSION_NOHASH = Version ''{0}'' contains a ''#'', which as this character is used in some URLs to separate the version and the fragment id. When version does include '#', systems will not be able to parse the URL PRIMITIVE_TOO_SHORT = Value ''{0}'' is shorter than permitted minimum length of {1} - \ No newline at end of file +CANONICAL_MULTIPLE_VERSIONS_KNOWN = The version {2} for the {0} {1} is not known. These versions are known: {3} +SD_PATH_SLICING_DEPRECATED = The discriminator type ''{0}'' has been deprecated. Use type=fixed with a pattern[x] instead +SD_PATH_NOT_VALID = The discriminator path ''{0}'' does not appear to be valid for the element that is being sliced ''{1}'' +SD_PATH_ERROR = The discriminator path ''{0}'' does not appear to be valid for the element that is being sliced ''{1}'': {2} + diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/HTTPHeaderTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/HTTPHeaderTests.java new file mode 100644 index 0000000000..ad86e35e8c --- /dev/null +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/HTTPHeaderTests.java @@ -0,0 +1,20 @@ +package org.hl7.fhir.utilities.http; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HTTPHeaderTests { + @Test + void testHTTPHeaderEquals() { + HTTPHeader header1 = new HTTPHeader("name", "value1"); + HTTPHeader sameAsHeader1 = new HTTPHeader("name", "value1"); + assertThat(header1).isEqualTo(sameAsHeader1); + + HTTPHeader notSameNameAsHeader1 = new HTTPHeader("name2", "value1"); + assertThat(header1).isNotEqualTo(notSameNameAsHeader1); + + HTTPHeader notSameValueAsHeader1 = new HTTPHeader("name", "value2"); + assertThat(header1).isNotEqualTo(notSameValueAsHeader1); + } +} diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessorTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessorTests.java new file mode 100644 index 0000000000..be68a2571b --- /dev/null +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessorTests.java @@ -0,0 +1,40 @@ +package org.hl7.fhir.utilities.http; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ManagedFhirWebAccessorTests { + final String expectedUserAgent = "dummy-agent"; + + @Test + @DisplayName("Test default headers are added correctly.") + void addDefaultAgentHeader() { + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com"); + + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(expectedUserAgent, null); + + HTTPRequest requestWithDefaultHeaders = builder.httpRequestWithDefaultHeaders(request); + assertRequestContainsExpectedAgentHeader(requestWithDefaultHeaders); + } + + @Test + @DisplayName("Test default headers are added correctly.") + void addDefaultBasicHeader() { + HTTPRequest request = new HTTPRequest().withUrl("http://www.google.com"); + + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor(expectedUserAgent, null) + .withBasicAuth("dummy-user", "dummy-password"); + + HTTPRequest requestWithManagedHeaders = builder.requestWithManagedHeaders(request); + assertRequestContainsExpectedAgentHeader(requestWithManagedHeaders); + + Assertions.assertNotNull(HTTPHeaderUtil.getSingleHeader(requestWithManagedHeaders.getHeaders(),"Authorization"), "Authorization header null."); + } + + private void assertRequestContainsExpectedAgentHeader(HTTPRequest requestWithDefaultHeaders) { + Assertions.assertNotNull(HTTPHeaderUtil.getSingleHeader(requestWithDefaultHeaders.getHeaders(),"User-Agent"), "User-Agent header null."); + Assertions.assertEquals(expectedUserAgent, HTTPHeaderUtil.getSingleHeader(requestWithDefaultHeaders.getHeaders(),"User-Agent"), + "User-Agent header not populated with expected value \""+expectedUserAgent+"\"."); + } +} diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java new file mode 100644 index 0000000000..f57d93988f --- /dev/null +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java @@ -0,0 +1,216 @@ +package org.hl7.fhir.utilities.http; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.net.util.Base64; +import org.hl7.fhir.utilities.settings.ServerDetailsPOJO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ManagedWebAccessAuthTests { + + public static final String DUMMY_AGENT = "dummyAgent"; + public static final String DUMMY_USERNAME = "dummy1"; + public static final String DUMMY_PASSWORD = "pass1"; + + public static final String DUMMY_TOKEN = "dummyToken"; + private static final String DUMMY_API_KEY = "dummyApiKey"; + private MockWebServer server; + + @BeforeEach + void setup() { + setupMockServer(); + } + + void setupMockServer() { + server = new MockWebServer(); + } + + @Test + public void testBaseCase() throws IOException, InterruptedException { + HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah"); + + server.enqueue( + new MockResponse() + .setBody("Dummy Response").setResponseCode(200) + ); + + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null); + HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET)); + + assertThat(result.getCode()).isEqualTo(200); + assertThat(result.getContentAsString()).isEqualTo("Dummy Response"); + + RecordedRequest packageRequest = server.takeRequest(); + + assert packageRequest.getRequestUrl() != null; + assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET"); + + } + + + @Test + public void testBasicAuthCase() throws IOException, InterruptedException { + + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null).withBasicAuth("dummy1", "pass1"); + + testBasicServerAuth(builder); + } + + private void testBasicServerAuth(ManagedFhirWebAccessor builder) throws IOException, InterruptedException { + HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah"); + + server.enqueue( + new MockResponse() + .setBody("Dummy Response").setResponseCode(200) + ); + HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET)); + + assertThat(result.getCode()).isEqualTo(200); + assertThat(result.getContentAsString()).isEqualTo("Dummy Response"); + + RecordedRequest packageRequest = server.takeRequest(); + + assert packageRequest.getRequestUrl() != null; + assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET"); + + byte[] b = Base64.encodeBase64((DUMMY_USERNAME + ":" + DUMMY_PASSWORD).getBytes(StandardCharsets.US_ASCII)); + String b64 = new String(b, StandardCharsets.US_ASCII); + + assertThat(packageRequest.getHeader("Authorization")).isEqualTo("Basic " + b64); + } + + @Test + public void testTokenAuthCase() throws IOException, InterruptedException { + + + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null).withToken(DUMMY_TOKEN); + + testTokenAuthCase(builder); + } + + private void testTokenAuthCase(ManagedFhirWebAccessor builder) throws IOException, InterruptedException { + HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah"); + + server.enqueue( + new MockResponse() + .setBody("Dummy Response").setResponseCode(200) + ); + HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET)); + + assertThat(result.getCode()).isEqualTo(200); + assertThat(result.getContentAsString()).isEqualTo("Dummy Response"); + + RecordedRequest packageRequest = server.takeRequest(); + + assert packageRequest.getRequestUrl() != null; + assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET"); + + assertThat(packageRequest.getHeader("Authorization")).isEqualTo("Bearer " + DUMMY_TOKEN); + } + + private void assertExpectedHeaders(RecordedRequest packageRequest, String expectedUrl, String expectedHttpMethod) { + assertThat(packageRequest.getRequestUrl().toString()).isEqualTo(expectedUrl); + assertThat(packageRequest.getMethod()).isEqualTo(expectedHttpMethod); + assertThat(packageRequest.getHeader("User-Agent")).isEqualTo(DUMMY_AGENT); + } + + @Test + public void testApiKeyAuthCase() throws IOException, InterruptedException { + + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor("dummyAgent", null).withApiKey(DUMMY_API_KEY); + + testApiKeyAuthCase(builder); + } + + private void testApiKeyAuthCase(ManagedFhirWebAccessor builder) throws IOException, InterruptedException { + HttpUrl serverUrl = server.url("blah/blah/blah?arg=blah"); + + server.enqueue( + new MockResponse() + .setBody("Dummy Response").setResponseCode(200) + ); + HTTPResult result = builder.httpCall(new HTTPRequest().withUrl(serverUrl.toString()).withMethod(HTTPRequest.HttpMethod.GET)); + + assertThat(result.getCode()).isEqualTo(200); + assertThat(result.getContentAsString()).isEqualTo("Dummy Response"); + + RecordedRequest packageRequest = server.takeRequest(); + + assert packageRequest.getRequestUrl() != null; + assertExpectedHeaders(packageRequest, serverUrl.url().toString(), "GET"); + + assertThat(packageRequest.getHeader("Api-Key")).isEqualTo(DUMMY_API_KEY); + } + + @Test + public void testBasicAuthFromSettings() throws IOException, InterruptedException { + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor( + "dummyAgent", + List.of(getBasicAuthServerPojo())); + + testBasicServerAuth(builder); + } + + private ServerDetailsPOJO getBasicAuthServerPojo() { + return new ServerDetailsPOJO( + server.url("").toString(), + "basic", + "dummyServerType", + DUMMY_USERNAME, + DUMMY_PASSWORD, + null, null); + } + +@Test +public void testTokenAuthFromSettings() throws IOException, InterruptedException { + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor( + "dummyAgent", + List.of(getTokenAuthServerPojo())); + + testTokenAuthCase(builder); +} + + private ServerDetailsPOJO getTokenAuthServerPojo() { + return new ServerDetailsPOJO( + server.url("").toString(), + "token", + "dummyServerType", + null, + null, + DUMMY_TOKEN, null); + } + + @Test + public void testApiKeyAuthFromSettings() throws IOException, InterruptedException { + ManagedFhirWebAccessor builder = new ManagedFhirWebAccessor( + "dummyAgent", + List.of(getApiKeyAuthServerPojo())); + + testApiKeyAuthCase(builder); + } + + private ServerDetailsPOJO getApiKeyAuthServerPojo() { + return new ServerDetailsPOJO( + server.url("").toString(), + "apikey", + "dummyServerType", + null, + null, + null, DUMMY_API_KEY); + } + + @Test + public void verifyAllowedPaths() { + //TODO the allowed paths cannot be set for now, meaning all will be allowed. + ManagedWebAccess.inAllowedPaths("http://www.anywhere.com"); + } +} \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java index 3684923b36..ff1d9d4de1 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java @@ -71,6 +71,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.utilities.TimeTracker; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.http.ManagedWebAccess; import org.hl7.fhir.utilities.settings.FhirSettings; import org.hl7.fhir.validation.cli.model.CliContext; import org.hl7.fhir.validation.cli.services.ValidationService; @@ -151,6 +152,7 @@ protected void readParamsAndExecuteTask(CliContext cliContext, String[] args) th if (cliContext.getFhirSettingsFile() != null) { FhirSettings.setExplicitFilePath(cliContext.getFhirSettingsFile()); } + ManagedWebAccess.loadFromFHIRSettings(); FileFormat.checkCharsetAndWarnIfNotUTF8(System.out); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 0b8f31c0c4..c598bae083 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -1047,7 +1047,8 @@ private void checkElementUsage(List errors, Element element, } private boolean check(String v1, String v2) { - return v1 == null ? Utilities.noString(v1) : v1.equals(v2); + boolean res = v1 == null ? Utilities.noString(v1) : v1.equals(v2); + return res; } private boolean checkAddress(List errors, String path, Element focus, Address fixed, String fixedSource, boolean pattern, String context) { @@ -3209,10 +3210,10 @@ else if (Utilities.isAllWhitespace(e.primitiveValue())) } if (context.hasFixed()) { - ok = checkFixedValue(errors, path, e, context.getFixed(), profile.getVersionedUrl(), context.getSliceName(), null, false, "") && ok; + ok = checkFixedValue(errors, path, e, context.getFixed(), profile.getVersionedUrl(), context.getSliceName(), null, false, profile.getVersionedUrl()+"#"+context.getId()) && ok; } if (context.hasPattern()) { - ok = checkFixedValue(errors, path, e, context.getPattern(), profile.getVersionedUrl(), context.getSliceName(), null, true, "") && ok; + ok = checkFixedValue(errors, path, e, context.getPattern(), profile.getVersionedUrl(), context.getSliceName(), null, true, profile.getVersionedUrl()+"#"+context.getId()) && ok; } if (ok && !ID_EXEMPT_LIST.contains(e.fhirType())) { // ids get checked elsewhere @@ -5129,12 +5130,13 @@ private boolean sliceMatches(ValidationContext valContext, Element element, Stri if ("0".equals(criteriaElement.getMax())) { expression.append(" and " + discriminator + ".empty()"); } else if (s.getType() == DiscriminatorType.TYPE) { - String type = null; if (!criteriaElement.getPath().contains("[") && discriminator.contains("[")) { discriminator = discriminator.substring(0, discriminator.indexOf('[')); String lastNode = tail(discriminator); - type = makeTypeForFHIRPath(criteriaElement.getPath()).substring(lastNode.length()); + String type = makeTypeForFHIRPath(criteriaElement.getPath()).substring(lastNode.length()); + expression.append(" and " + discriminator + " is " + type); } else if (!criteriaElement.hasType() || criteriaElement.getType().size() == 1) { + String type = null; if (discriminator.contains("[")) discriminator = discriminator.substring(0, discriminator.indexOf('[')); if (criteriaElement.hasType()) { @@ -5144,23 +5146,25 @@ private boolean sliceMatches(ValidationContext valContext, Element element, Stri } else { throw new DefinitionException(context.formatMessage(I18nConstants.DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_NO_TYPES, discriminator, ed.getId(), profile.getVersionedUrl())); } + expression.append(" and " + discriminator + " is " + type); } else if (criteriaElement.getType().size() > 1) { - throw new DefinitionException(context.formatMessagePlural(criteriaElement.getType().size(), I18nConstants.DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_MULTIPLE_TYPES, discriminator, ed.getId(), profile.getVersionedUrl(), criteriaElement.typeSummary())); + CommaSeparatedStringBuilder cb = new CommaSeparatedStringBuilder(" or "); + for (TypeRefComponent tr : criteriaElement.getType()) { + String type = makeTypeForFHIRPath(tr.getWorkingCode()); + cb.append(discriminator + " is " + type); + } + expression.append(" and (" + cb.toString()+")"); } else throw new DefinitionException(context.formatMessage(I18nConstants.DISCRIMINATOR__IS_BASED_ON_TYPE_BUT_SLICE__IN__HAS_NO_TYPES, discriminator, ed.getId(), profile.getVersionedUrl())); - if (discriminator.isEmpty()) { - expression.append(" and $this is " + type); - } else { - expression.append(" and " + discriminator + " is " + type); - } } else if (s.getType() == DiscriminatorType.PROFILE) { if (criteriaElement.getType().size() == 0) { throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE__IN_PROFILE_, criteriaElement.getId(), profile.getVersionedUrl())); } - if (criteriaElement.getType().size() != 1) { - throw new DefinitionException(context.formatMessagePlural(criteriaElement.getType().size(), I18nConstants.PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_ONLY_ONE_TYPE__IN_PROFILE, criteriaElement.getId(), profile.getVersionedUrl())); + List list = new ArrayList<>(); + boolean ref = discriminator.endsWith(".resolve()") || discriminator.equals("resolve()"); + for (TypeRefComponent tr : criteriaElement.getType()) { + list.addAll(ref ? tr.getTargetProfile() : tr.getProfile()); } - List list = discriminator.endsWith(".resolve()") || discriminator.equals("resolve()") ? criteriaElement.getType().get(0).getTargetProfile() : criteriaElement.getType().get(0).getProfile(); if (list.size() == 0) { // we don't have to find something // throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE_BASED_DISCRIMINATORS_MUST_HAVE_A_TYPE_WITH_A_PROFILE__IN_PROFILE_, criteriaElement.getId(), profile.getVersionedUrl())); @@ -6357,10 +6361,10 @@ private boolean validateElement(ValidationContext valContext, List msgs = new ArrayList(); - checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getVersionedUrl(), "value", null, false, ""); + checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), profile.getVersionedUrl(), "value", null, false, profile.getVersionedUrl()+"#"+criteria.getId()); return msgs.size() == 0; } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) { throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SLICE_MATCHING__SLICE_MATCHING_BY_VALUE_SET_NOT_DONE)); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java index ce526675eb..2333cfc780 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java @@ -237,6 +237,17 @@ public boolean validateMeasureReport(ValidationContext hostContext, List versionList = context.fetchResourcesByUrl(Measure.class, measure.substring(0, measure.indexOf("|"))); + if (versionList != null && !versionList.isEmpty()) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (Measure mm : versionList) { + b.append(mm.getVersion()); + } + hint(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, m.line(), m.col(), stack.getLiteralPath(), msrc != null, I18nConstants.CANONICAL_MULTIPLE_VERSIONS_KNOWN, "Measure", measure, measure.substring(measure.indexOf("|")+1), b.toString()); + } + } } } return ok; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java index 13ef234a0f..1acff417c8 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java @@ -16,6 +16,7 @@ import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Manager; @@ -23,6 +24,7 @@ import org.hl7.fhir.r5.extensions.ExtensionConstants; import org.hl7.fhir.r5.fhirpath.ExpressionNode; import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus; +import org.hl7.fhir.r5.fhirpath.FHIRLexer.FHIRLexerException; import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IssueMessage; import org.hl7.fhir.r5.fhirpath.TypeDetails; @@ -193,7 +195,7 @@ public boolean validateStructureDefinition(List errors, Eleme } } } catch (Exception e) { - //e.printStackTrace(); + e.printStackTrace(); rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage()); ok = false; } @@ -445,6 +447,44 @@ private boolean validateElementDefinition(List errors, List 1, but we can't always know that on the differential - though we can look it up + String tn = path.contains(".") ? path.substring(0, path.indexOf(".")) : path; + StructureDefinition tsd = context.fetchTypeDefinition(tn); + ElementDefinition ted = null; + if (tsd != null) { + ted = tsd.getSnapshot().getElementByPath(path); + if (ted != null) { + ok = rule(errors, "2022-11-02", IssueType.NOTFOUND, sStack, canSlice(ted), I18nConstants.SD_PATH_NO_SLICING, path) && ok; + } + } + int i = 0; + for (Element discriminator : slicing.getChildren("discriminator")) { + NodeStack dStack = sStack.push(discriminator, i, null, null); + String type = discriminator.getNamedChildValue("type"); + warning(errors, "2024-11-06", IssueType.BUSINESSRULE, dStack, !"pattern".equals(type), I18nConstants.SD_PATH_SLICING_DEPRECATED, type); + String pathExp = discriminator.getNamedChildValue("path"); + if (ted != null) { + TypeDetails td = getTypesForElement(elements, element, tn, tsd.getUrl()); + if (!td.isEmpty()) { + List warnings = new ArrayList(); + try { + TypeDetails eval = fpe.checkOnTypes(this, tn, td, fpe.parse(pathExp), warnings, true); + if (eval.isEmpty()) { + ok = rule(errors, "2024-11-06", IssueType.INVALID, dStack, false, I18nConstants.SD_PATH_NOT_VALID, pathExp, path) && ok; + } + } catch (Exception e) { + ok = rule(errors, "2024-11-06", IssueType.INVALID, dStack, false, I18nConstants.SD_PATH_ERROR, pathExp, path, e.getMessage()) && ok; + } + } + } + } + } if (!snapshot && (element.hasChild("fixed") || element.hasChild("pattern")) && base != null) { ElementDefinition ed = getDefinitionFromBase(base, element.getNamedChildValue("id"), element.getNamedChildValue("path")); @@ -595,6 +635,10 @@ private boolean validateElementDefinition(List errors, List errors, NodeStack stack, Element typeE, String tc, StructureDefinition tsd, String path, StructureDefinition sd) { boolean ok = true; if (tsd.hasExtension(ToolingExtensions.EXT_TYPE_PARAMETER)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java index 36708c84ae..896fb2d758 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/special/TxTester.java @@ -4,7 +4,6 @@ import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; @@ -26,7 +25,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50; import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; import org.hl7.fhir.convertors.txClient.TerminologyClientFactory; import org.hl7.fhir.exceptions.DefinitionException; @@ -41,19 +39,14 @@ import org.hl7.fhir.r5.terminologies.client.ITerminologyClient; import org.hl7.fhir.r5.test.utils.CompareUtilities; import org.hl7.fhir.r5.utils.client.EFhirClientException; -import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; -import org.hl7.fhir.utilities.FhirPublication; -import org.hl7.fhir.utilities.TextFile; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.VersionUtil; +import org.hl7.fhir.utilities.*; import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; +import org.hl7.fhir.utilities.http.HTTPHeader; import org.hl7.fhir.utilities.json.JsonException; import org.hl7.fhir.utilities.json.model.JsonArray; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.parser.JsonParser; -import okhttp3.internal.http2.Header; - public class TxTester { @@ -134,7 +127,7 @@ public boolean execute(String version, List modes, String filter) throws return true; } else { System.out.println(software+" did not pass all HL7 terminology service tests (modes "+m+", tests v"+loadVersion()+", runner v"+VersionUtil.getBaseVersion()+")"); - System.out.println("Failed Tests: "+CommaSeparatedStringBuilder.join(",", fails )); + System.out.println("Failed Tests: "+ CommaSeparatedStringBuilder.join(",", fails )); return false; } } else { @@ -250,12 +243,12 @@ private boolean runTest(JsonObject suite, JsonObject test, ITerminologyClient tx outputT.add("name", test.asString("name")); if (Utilities.noString(filter) || filter.equals("*") || test.asString("name").contains(filter)) { System.out.print(" Test "+test.asString("name")+": "); - Header header = null; + HTTPHeader header = null; try { if (test.has("header")) { JsonObject hdr = test.getJsonObject("header"); if (hdr.has("mode") && modes.contains(hdr.asString("mode"))) { - header = new Header(hdr.asString("name"), hdr.asString("value")); + header = new HTTPHeader(hdr.asString("name"), hdr.asString("value")); tx.getClientHeaders().addHeader(header); } } diff --git a/org.hl7.fhir.validation/src/test/java/ManagedWebAccessAuthTests.java b/org.hl7.fhir.validation/src/test/java/ManagedWebAccessAuthTests.java new file mode 100644 index 0000000000..ce57a67bc3 --- /dev/null +++ b/org.hl7.fhir.validation/src/test/java/ManagedWebAccessAuthTests.java @@ -0,0 +1,9 @@ +import org.hl7.fhir.utilities.http.ManagedWebAccess; +import org.junit.BeforeClass; + +public class ManagedWebAccessAuthTests { + @BeforeClass + public static void setUp() { + ManagedWebAccess.setUserAgent("hapi-fhir-testing-client"); + } +} diff --git a/pom.xml b/pom.xml index f6bd4d8509..e408832b0c 100644 --- a/pom.xml +++ b/pom.xml @@ -434,7 +434,7 @@ while if true it will use an executable. --> true 512m - 768m + 1000m true -J-XX:MaxRAMPercentage=50.0 diff --git a/setup-and-cache-job-template.yml b/setup-and-cache-job-template.yml index a5100e4ed2..3469e06e69 100644 --- a/setup-and-cache-job-template.yml +++ b/setup-and-cache-job-template.yml @@ -28,7 +28,7 @@ jobs: options: '--settings $(Agent.TempDirectory)/settings.xml -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER) -DskipTests -DdeployToSonatype -P CHECKSTYLE' ${{ else }}: options: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER) -DskipTests -P CHECKSTYLE' - mavenOptions: '-Xmx768m' + mavenOptions: '-Xmx1G' javaHomeOption: 'JDKVersion' jdkVersionOption: '1.11' jdkArchitectureOption: 'x64'