From 7e5a670846186ced8faedb5ad8d3c411194521b3 Mon Sep 17 00:00:00 2001 From: Mark Iantorno Date: Tue, 24 Nov 2020 09:20:43 -0500 Subject: [PATCH] Http client update (#378) * wip * tests all pass with log outputs identical to original...is it better now? I don't know. * proxy works, adding a test and docs next * more cleanup and test fixes * left in testing file * didn't need that code anymore --- org.hl7.fhir.r5/pom.xml | 12 + .../hl7/fhir/r5/utils/client/ClientUtils.java | 684 ----------- .../r5/utils/client/EFhirClientException.java | 1 - .../r5/utils/client/FHIRToolingClient.java | 1010 +++++------------ .../fhir/r5/utils/client/ResourceAddress.java | 3 - .../fhir/r5/utils/client/ResourceRequest.java | 109 -- .../r5/utils/client/network/ByteUtils.java | 69 ++ .../fhir/r5/utils/client/network/Client.java | 234 ++++ .../client/network/FhirRequestBuilder.java | 339 ++++++ .../utils/client/network/ResourceRequest.java | 71 ++ .../client/network/RetryInterceptor.java | 63 + .../r5/utils/client/network/ClientTest.java | 146 +++ .../network/FhirRequestBuilderTest.java | 147 +++ .../org/hl7/fhir/validation/ValidatorCli.java | 46 +- .../hl7/fhir/validation/cli/utils/Params.java | 4 + 15 files changed, 1443 insertions(+), 1495 deletions(-) delete mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ClientUtils.java delete mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceRequest.java create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ByteUtils.java create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/Client.java create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilder.java create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ResourceRequest.java create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/RetryInterceptor.java create mode 100644 org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientTest.java create mode 100644 org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilderTest.java diff --git a/org.hl7.fhir.r5/pom.xml b/org.hl7.fhir.r5/pom.xml index 4721999c4..f77ef03fd 100644 --- a/org.hl7.fhir.r5/pom.xml +++ b/org.hl7.fhir.r5/pom.xml @@ -101,6 +101,11 @@ httpclient true + + com.squareup.okhttp3 + okhttp + 4.9.0 + @@ -119,6 +124,13 @@ ${validator_test_case_version} test + + com.squareup.okhttp3 + mockwebserver + 4.9.0 + test + + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ClientUtils.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ClientUtils.java deleted file mode 100644 index 1688c010f..000000000 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ClientUtils.java +++ /dev/null @@ -1,684 +0,0 @@ -package org.hl7.fhir.r5.utils.client; - - - - - -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - */ - - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URLConnection; -import java.nio.charset.StandardCharsets; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.Header; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpOptions; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.params.ConnRoutePNames; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.hl7.fhir.r5.formats.IParser; -import org.hl7.fhir.r5.formats.IParser.OutputStyle; -import org.hl7.fhir.r5.formats.JsonParser; -import org.hl7.fhir.r5.formats.XmlParser; -import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.OperationOutcome; -import org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity; -import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; -import org.hl7.fhir.r5.model.Resource; -import org.hl7.fhir.r5.model.ResourceType; -import org.hl7.fhir.r5.utils.ResourceUtilities; -import org.hl7.fhir.utilities.ToolingClientLogger; -import org.hl7.fhir.utilities.Utilities; - -/** - * Helper class handling lower level HTTP transport concerns. - * TODO Document methods. - * @author Claude Nanjo - */ -public class ClientUtils { - - public static final String DEFAULT_CHARSET = "UTF-8"; - public static final String HEADER_LOCATION = "location"; - private static boolean debugging = false; - public static final int TIMEOUT_SOCKET = 5000; - public static final int TIMEOUT_CONNECT = 1000; - - private HttpHost proxy; - private int timeout = TIMEOUT_SOCKET; - private String username; - private String password; - private ToolingClientLogger logger; - private int retryCount; - private HttpClient httpclient; - - public HttpHost getProxy() { - return proxy; - } - - public void setProxy(HttpHost proxy) { - this.proxy = proxy; - } - - public int getTimeout() { - return timeout; - } - - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public ResourceRequest issueOptionsRequest(URI optionsUri, String resourceFormat, String message, int timeout) { - HttpOptions options = new HttpOptions(optionsUri); - return issueResourceRequest(resourceFormat, options, message, timeout); - } - - public ResourceRequest issueGetResourceRequest(URI resourceUri, String resourceFormat, String message, int timeout) { - HttpGet httpget = new HttpGet(resourceUri); - return issueResourceRequest(resourceFormat, httpget, message, timeout); - } - - public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List
headers, String message, int timeout) { - HttpPut httpPut = new HttpPut(resourceUri); - return issueResourceRequest(resourceFormat, httpPut, payload, headers, message, timeout); - } - - public ResourceRequest issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) { - HttpPut httpPut = new HttpPut(resourceUri); - return issueResourceRequest(resourceFormat, httpPut, payload, null, message, timeout); - } - - public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List
headers, String message, int timeout) { - HttpPost httpPost = new HttpPost(resourceUri); - return issueResourceRequest(resourceFormat, httpPost, payload, headers, message, timeout); - } - - - public ResourceRequest issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) { - return issuePostRequest(resourceUri, payload, resourceFormat, null, message, timeout); - } - - public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) { - 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) { - } - } - } - - public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, String message, int timeout) { - HttpPost httpPost = new HttpPost(resourceUri); - configureFhirRequest(httpPost, resourceFormat); - HttpResponse response = sendPayload(httpPost, payload, proxy, message, timeout); - return unmarshalFeed(response, resourceFormat); - } - - public boolean issueDeleteRequest(URI resourceUri) { - HttpDelete deleteRequest = new HttpDelete(resourceUri); - HttpResponse response = sendRequest(deleteRequest); - int responseStatusCode = response.getStatusLine().getStatusCode(); - boolean deletionSuccessful = false; - if(responseStatusCode == 204) { - deletionSuccessful = true; - } - return deletionSuccessful; - } - - /*********************************************************** - * Request/Response Helper methods - ***********************************************************/ - - protected ResourceRequest issueResourceRequest(String resourceFormat, HttpUriRequest request, String message, int timeout) { - return issueResourceRequest(resourceFormat, request, null, message, timeout); - } - - /** - * @param resourceFormat - * @param options - * @return - */ - protected ResourceRequest issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, String message, int timeout) { - return issueResourceRequest(resourceFormat, request, payload, null, message, timeout); - } - - /** - * @param resourceFormat - * @param options - * @return - */ - protected ResourceRequest issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List
headers, String message, int timeout) { - configureFhirRequest(request, resourceFormat, headers); - HttpResponse response = null; - if(request instanceof HttpEntityEnclosingRequest && payload != null) { - response = sendPayload((HttpEntityEnclosingRequestBase)request, payload, proxy, message, timeout); - } else if (request instanceof HttpEntityEnclosingRequest && payload == null){ - throw new EFhirClientException("PUT and POST requests require a non-null payload"); - } else { - response = sendRequest(request); - } - T resource = unmarshalReference(response, resourceFormat); - return new ResourceRequest(resource, response.getStatusLine().getStatusCode(), getLocationHeader(response)); - } - - - /** - * Method adds required request headers. - * TODO handle JSON request as well. - * - * @param request - */ - protected void configureFhirRequest(HttpRequest request, String format) { - configureFhirRequest(request, format, null); - } - - /** - * Method adds required request headers. - * TODO handle JSON request as well. - * - * @param request - */ - protected void configureFhirRequest(HttpRequest request, String format, List
headers) { - request.addHeader("User-Agent", "hapi-fhir-tooling-client"); - - if (format != null) { - request.addHeader("Accept",format); - request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); - } - request.addHeader("Accept-Charset", DEFAULT_CHARSET); - if(headers != null) { - for(Header header : headers) { - request.addHeader(header); - } - } - setAuth(request); - } - - /** - * Method posts request payload - * - * @param request - * @param payload - * @return - */ - @SuppressWarnings({ "resource", "deprecation" }) - protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy, String message, int timeout) { - HttpResponse response = null; - boolean ok = false; - long t = System.currentTimeMillis(); - int tryCount = 0; - while (!ok) { - try { - tryCount++; - if (httpclient == null) { - makeClient(proxy); - } - HttpParams params = httpclient.getParams(); - HttpConnectionParams.setSoTimeout(params, timeout < 1 ? this.timeout : timeout * 1000); - 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)+" for "+message+")"); - if (tryCount <= retryCount || (tryCount < 3 && ioe instanceof org.apache.http.conn.ConnectTimeoutException)) { - ok = false; - try { - Thread.sleep(100); - } catch (InterruptedException e) { - } - } else { - if (tryCount > 4) { - System.out.println("Giving up: "+ioe.getMessage()+" (R5 / "+(System.currentTimeMillis()-t)+"ms / "+Utilities.describeSize(payload.length)+" for "+message+")"); - } - throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe); - } - } - } - return response; - } - - @SuppressWarnings("deprecation") - public void makeClient(HttpHost proxy) { - httpclient = new DefaultHttpClient(); - HttpParams params = httpclient.getParams(); - HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_CONNECT); - HttpConnectionParams.setSoTimeout(params, timeout); - HttpConnectionParams.setSoKeepalive(params, true); - if(proxy != null) { - httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); - } - } - - /** - * - * @param request - * @param payload - * @return - */ - protected HttpResponse sendRequest(HttpUriRequest request) { - HttpResponse response = null; - try { - if (httpclient == null) { - makeClient(proxy); - } - response = httpclient.execute(request); - } 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 - */ - @SuppressWarnings("unchecked") - protected T unmarshalReference(HttpResponse response, String format) { - T resource = null; - OperationOutcome error = null; - byte[] cnt = log(response); - if (cnt != null) { - try { - resource = (T)getParser(format).parse(cnt); - if (resource instanceof OperationOutcome && hasError((OperationOutcome)resource)) { - error = (OperationOutcome) resource; - } - } catch(IOException ioe) { - throw new EFhirClientException("Error reading Http Response: "+ioe.getMessage(), ioe); - } catch(Exception e) { - throw new EFhirClientException("Error parsing response message: "+e.getMessage(), e); - } - } - if(error != null) { - throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error); - } - return resource; - } - - /** - * Unmarshals Bundle from response stream. - * - * @param response - * @return - */ - protected Bundle unmarshalFeed(HttpResponse response, String format) { - Bundle feed = null; - byte[] cnt = log(response); - String contentType = response.getHeaders("Content-Type")[0].getValue(); - OperationOutcome error = null; - try { - if (cnt != null) { - if(contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) { - Resource rf = getParser(format).parse(cnt); - if (rf instanceof Bundle) - feed = (Bundle) rf; - else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { - error = (OperationOutcome) rf; - } else { - throw new EFhirClientException("Error reading server response: a resource was returned instead"); - } - } - } - } catch(IOException ioe) { - throw new EFhirClientException("Error reading Http Response", ioe); - } catch(Exception e) { - throw new EFhirClientException("Error parsing response message", e); - } - if(error != null) { - throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error); - } - return feed; - } - - private 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(); - } - return location; - } - - - /***************************************************************** - * Client connection methods - * ***************************************************************/ - - public HttpURLConnection buildConnection(URI baseServiceUri, String tail) { - try { - HttpURLConnection client = (HttpURLConnection) baseServiceUri.resolve(tail).toURL().openConnection(); - return client; - } catch(MalformedURLException mue) { - throw new EFhirClientException("Invalid Service URL", mue); - } catch(IOException ioe) { - throw new EFhirClientException("Unable to establish connection to server: " + baseServiceUri.toString() + tail, ioe); - } - } - - public HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) { - return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id)); - } - - /****************************************************************** - * Other general helper methods - * ****************************************************************/ - - - public byte[] getResourceAsByteArray(T resource, boolean pretty, boolean isJson) { - ByteArrayOutputStream baos = null; - byte[] byteArray = null; - try { - baos = new ByteArrayOutputStream(); - IParser parser = null; - if(isJson) { - parser = new JsonParser(); - } else { - parser = new XmlParser(); - } - parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); - parser.compose(baos, resource); - baos.close(); - byteArray = baos.toByteArray(); - baos.close(); - } catch (Exception e) { - try{ - baos.close(); - }catch(Exception ex) { - throw new EFhirClientException("Error closing output stream", ex); - } - throw new EFhirClientException("Error converting output stream to byte array", e); - } - return byteArray; - } - - public byte[] getFeedAsByteArray(Bundle feed, boolean pretty, boolean isJson) { - ByteArrayOutputStream baos = null; - byte[] byteArray = null; - try { - baos = new ByteArrayOutputStream(); - IParser parser = null; - if(isJson) { - parser = new JsonParser(); - } else { - parser = new XmlParser(); - } - parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); - parser.compose(baos, feed); - baos.close(); - byteArray = baos.toByteArray(); - baos.close(); - } catch (Exception e) { - try{ - baos.close(); - }catch(Exception ex) { - throw new EFhirClientException("Error closing output stream", ex); - } - throw new EFhirClientException("Error converting output stream to byte array", e); - } - return byteArray; - } - - public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) { - String dateTime = null; - try { - dateTime = serverConnection.getHeaderField("Last-Modified"); - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", new Locale("en", "US")); - Date lastModifiedTimestamp = format.parse(dateTime); - Calendar calendar=Calendar.getInstance(); - calendar.setTime(lastModifiedTimestamp); - return calendar; - } catch(ParseException pe) { - throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe); - } - } - - protected IParser getParser(String format) { - if(StringUtils.isBlank(format)) { - format = ResourceFormat.RESOURCE_XML.getHeader(); - } - if(format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) { - return new JsonParser(); - } else if(format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) { - return new XmlParser(); - } else { - throw new EFhirClientException("Invalid format: " + format); - } - } - - public Bundle issuePostFeedRequest(URI resourceUri, Map parameters, String resourceName, Resource resource, String resourceFormat) throws IOException { - HttpPost httppost = new HttpPost(resourceUri); - 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)); - return unmarshalFeed(response, resourceFormat); - } - - private byte[] encodeFormSubmission(Map parameters, String resourceName, Resource resource, String boundary) throws IOException { - ByteArrayOutputStream b = new ByteArrayOutputStream(); - OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8"); - for (String name : parameters.keySet()) { - w.write("--"); - w.write(boundary); - w.write("\r\nContent-Disposition: form-data; name=\""+name+"\"\r\n\r\n"); - w.write(parameters.get(name)+"\r\n"); - } - w.write("--"); - w.write(boundary); - w.write("\r\nContent-Disposition: form-data; name=\""+resourceName+"\"\r\n\r\n"); - w.close(); - JsonParser json = new JsonParser(); - json.setOutputStyle(OutputStyle.NORMAL); - json.compose(b, resource); - b.close(); - w = new OutputStreamWriter(b, "UTF-8"); - w.write("\r\n--"); - w.write(boundary); - w.write("--"); - w.close(); - return b.toByteArray(); - } - - /** - * Method posts request payload - * - * @param request - * @param payload - * @return - */ - protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) { - HttpResponse response = null; - try { - log(request); - if (httpclient == null) { - makeClient(proxy); - } - request.setEntity(new ByteArrayEntity(payload)); - response = httpclient.execute(request); - log(response); - } 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); - } - return cnt; - } - - public ToolingClientLogger getLogger() { - return logger; - } - - public void setLogger(ToolingClientLogger logger) { - this.logger = logger; - } - - - /** - * Used for debugging - * - * @param instream - * @return - */ - protected String writeInputStreamAsString(InputStream instream) { - String value = null; - try { - value = IOUtils.toString(instream, "UTF-8"); - System.out.println(value); - - } catch(IOException ioe) { - //Do nothing - } - return value; - } - - public int getRetryCount() { - return retryCount; - } - - public void setRetryCount(int retryCount) { - this.retryCount = retryCount; - } - - -} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/EFhirClientException.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/EFhirClientException.java index 8171150bc..8b01df1ac 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/EFhirClientException.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/EFhirClientException.java @@ -99,7 +99,6 @@ public class EFhirClientException extends RuntimeException { * A default message of "One or more server side errors have occurred during this operation. Refer to e.getServerErrors() for additional details." * will be returned to users. * - * @param message * @param serverError */ public EFhirClientException(OperationOutcome serverError) { 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 1e9c94af7..51eee8a1f 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 @@ -29,739 +29,374 @@ package org.hl7.fhir.r5.utils.client; */ -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.http.Header; -import org.apache.http.HttpHost; -import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.CapabilityStatement; -import org.hl7.fhir.r5.model.CodeSystem; -import org.hl7.fhir.r5.model.Coding; -import org.hl7.fhir.r5.model.ConceptMap; -import org.hl7.fhir.r5.model.OperationOutcome; -import org.hl7.fhir.r5.model.Parameters; +import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; -import org.hl7.fhir.r5.model.PrimitiveType; -import org.hl7.fhir.r5.model.Resource; -import org.hl7.fhir.r5.model.StringType; -import org.hl7.fhir.r5.model.TerminologyCapabilities; -import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.utils.client.network.ByteUtils; +import org.hl7.fhir.r5.utils.client.network.Client; +import org.hl7.fhir.r5.utils.client.network.ResourceRequest; import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.Utilities; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + /** - * Very Simple RESTful client. This is purely for use in the standalone + * Very Simple RESTful client. This is purely for use in the standalone * tools jar packages. It doesn't support many features, only what the tools * need. - * + *

* To use, initialize class and set base service URI as follows: - * + * *


  * FHIRSimpleClient fhirClient = new FHIRSimpleClient();
  * fhirClient.initialize("http://my.fhir.domain/myServiceRoot");
  * 
- * + *

* Default Accept and Content-Type headers are application/fhir+xml and application/fhir+json. - * + *

* These can be changed by invoking the following setter functions: - * + * *


  * setPreferredResourceFormat()
  * setPreferredFeedFormat()
  * 
- * - * TODO Review all sad paths. - * - * @author Claude Nanjo + *

+ * TODO Review all sad paths. * + * @author Claude Nanjo */ public class FHIRToolingClient { - - public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssK"; - public static final String DATE_FORMAT = "yyyy-MM-dd"; - public static final String hostKey = "http.proxyHost"; - public static final String portKey = "http.proxyPort"; - private static final int TIMEOUT_NORMAL = 15; - private static final int TIMEOUT_OPERATION = 30; - private static final int TIMEOUT_OPERATION_LONG = 60; - private static final int TIMEOUT_OPERATION_EXPAND = 120; - - private String base; - private ResourceAddress resourceAddress; - private ResourceFormat preferredResourceFormat; - private int maxResultSetSize = -1;//_count - private CapabilityStatement capabilities; - private ClientUtils utils = new ClientUtils(); - - //Pass enpoint for client - URI + public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssK"; + public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String hostKey = "http.proxyHost"; + public static final String portKey = "http.proxyPort"; + + private static final int TIMEOUT_NORMAL = 1500; + private static final int TIMEOUT_OPERATION = 30000; + private static final int TIMEOUT_OPERATION_LONG = 60000; + private static final int TIMEOUT_OPERATION_EXPAND = 120000; + + private String base; + private ResourceAddress resourceAddress; + private ResourceFormat preferredResourceFormat; + private int maxResultSetSize = -1;//_count + private CapabilityStatement capabilities; + private Client client = new Client(); + + //Pass enpoint for client - URI public FHIRToolingClient(String baseServiceUrl) throws URISyntaxException { preferredResourceFormat = ResourceFormat.RESOURCE_XML; - detectProxy(); initialize(baseServiceUrl); } - - public FHIRToolingClient(String baseServiceUrl, String username, String password) throws URISyntaxException { - preferredResourceFormat = ResourceFormat.RESOURCE_XML; - 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); - this.maxResultSetSize = -1; - checkCapabilities(); - } - - private void checkCapabilities() { - try { + public void initialize(String baseServiceUrl) throws URISyntaxException { + base = baseServiceUrl; + resourceAddress = new ResourceAddress(baseServiceUrl); + this.maxResultSetSize = -1; + checkCapabilities(); + } + + private void checkCapabilities() { + try { capabilities = getCapabilitiesStatementQuick(); - } catch (Throwable e) { - } - } + } catch (Throwable e) { + } + } public String getPreferredResourceFormat() { return preferredResourceFormat.getHeader(); } - - public void setPreferredResourceFormat(ResourceFormat resourceFormat) { - preferredResourceFormat = resourceFormat; - } - - public int getMaximumRecordCount() { - return maxResultSetSize; - } - - public void setMaximumRecordCount(int maxResultSetSize) { - this.maxResultSetSize = maxResultSetSize; - } - - public TerminologyCapabilities getTerminologyCapabilities() { - return (TerminologyCapabilities) utils.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), getPreferredResourceFormat(), "TerminologyCapabilities", TIMEOUT_NORMAL).getReference(); - } - - public CapabilityStatement getCapabilitiesStatement() { - CapabilityStatement conformance = null; - try { - conformance = (CapabilityStatement)utils.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false), getPreferredResourceFormat(), "CapabilitiesStatement", TIMEOUT_NORMAL).getReference(); - } catch(Exception e) { - handleException("An error has occurred while trying to fetch the server's conformance statement", e); - } - return conformance; - } - - public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException { - if (capabilities != null) - return capabilities; + + public void setPreferredResourceFormat(ResourceFormat resourceFormat) { + preferredResourceFormat = resourceFormat; + } + + public int getMaximumRecordCount() { + return maxResultSetSize; + } + + public void setMaximumRecordCount(int maxResultSetSize) { + this.maxResultSetSize = maxResultSetSize; + } + + public TerminologyCapabilities getTerminologyCapabilities() { + TerminologyCapabilities capabilities = null; try { - capabilities = (CapabilityStatement)utils.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true), getPreferredResourceFormat(), "CapabilitiesStatement-Quick", TIMEOUT_NORMAL).getReference(); - } catch(Exception e) { + capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(), + getPreferredResourceFormat(), "TerminologyCapabilities", TIMEOUT_NORMAL).getReference(); + } catch (Exception e) { + handleException("An error has occurred while trying to fetch the server's terminology capabilities", e); + } + return capabilities; + } + + public CapabilityStatement getCapabilitiesStatement() { + CapabilityStatement conformance = null; + try { + conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false), + getPreferredResourceFormat(), "CapabilitiesStatement", TIMEOUT_NORMAL).getReference(); + } catch (Exception e) { + handleException("An error has occurred while trying to fetch the server's conformance statement", e); + } + return conformance; + } + + public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException { + if (capabilities != null) return capabilities; + try { + capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true), + getPreferredResourceFormat(), "CapabilitiesStatement-Quick", TIMEOUT_NORMAL).getReference(); + } catch (Exception e) { handleException("An error has occurred while trying to fetch the server's conformance statement", e); } return capabilities; } - - public T read(Class resourceClass, String id) {//TODO Change this to AddressableResource - ResourceRequest result = null; - try { - result = utils.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), getPreferredResourceFormat(), "Read "+resourceClass.getName()+"/"+id, TIMEOUT_NORMAL); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addSuccessStatus(200);//Only one for now - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); - } - } catch (Exception e) { - handleException("An error has occurred while trying to read this resource", e); - } - return result.getPayload(); - } - public T vread(Class resourceClass, String id, String version) { - ResourceRequest result = null; - try { - result = utils.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version), getPreferredResourceFormat(), "VRead "+resourceClass.getName()+"/"+id+"/?_history/"+version, TIMEOUT_NORMAL); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405);//unknown - result.addSuccessStatus(200);//Only one for now - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); - } - } catch (Exception e) { - handleException("An error has occurred while trying to read this version of the resource", e); - } - return result.getPayload(); - } - - // GET fhir/ValueSet?url=http://hl7.org/fhir/ValueSet/clinical-findings&version=0.8 + public T read(Class resourceClass, String id) {//TODO Change this to AddressableResource + ResourceRequest result = null; + try { + result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), + getPreferredResourceFormat(), "Read " + resourceClass.getName() + "/" + id, TIMEOUT_NORMAL); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); + } + } catch (Exception e) { + handleException("An error has occurred while trying to read this resource", e); + } + return result.getPayload(); + } + + public T vread(Class resourceClass, String id, String version) { + ResourceRequest result = null; + try { + result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version), + getPreferredResourceFormat(), "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, TIMEOUT_NORMAL); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); + } + } catch (Exception e) { + handleException("An error has occurred while trying to read this version of the resource", e); + } + return result.getPayload(); + } public T getCanonical(Class resourceClass, String canonicalURL) { ResourceRequest result = null; try { - result = utils.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL), getPreferredResourceFormat(), "Read "+resourceClass.getName()+"?url="+canonicalURL, TIMEOUT_NORMAL); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405);//unknown - result.addSuccessStatus(200);//Only one for now - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL), + getPreferredResourceFormat(), "Read " + resourceClass.getName() + "?url=" + canonicalURL, TIMEOUT_NORMAL); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } } catch (Exception e) { handleException("An error has occurred while trying to read this version of the resource", e); } Bundle bnd = (Bundle) result.getPayload(); if (bnd.getEntry().size() == 0) - throw new EFhirClientException("No matching resource found for canonical URL '"+canonicalURL+"'"); + throw new EFhirClientException("No matching resource found for canonical URL '" + canonicalURL + "'"); if (bnd.getEntry().size() > 1) - throw new EFhirClientException("Multiple matching resources found for canonical URL '"+canonicalURL+"'"); + throw new EFhirClientException("Multiple matching resources found for canonical URL '" + canonicalURL + "'"); return (T) bnd.getEntry().get(0).getResource(); } - - + public Resource update(Resource resource) { - ResourceRequest result = null; + org.hl7.fhir.r5.utils.client.network.ResourceRequest result = null; try { - List

headers = null; - result = utils.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), headers, - "Update "+resource.fhirType()+"/"+resource.getId(), TIMEOUT_OPERATION); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()), + ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), + "Update " + resource.fhirType() + "/" + resource.getId(), TIMEOUT_OPERATION); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } - } catch(Exception e) { + } catch (Exception e) { throw new EFhirClientException("An error has occurred while trying to update this resource", e); } // TODO oe 26.1.2015 could be made nicer if only OperationOutcome locationheader is returned with an operationOutcome would be returned (and not the resource also) we make another read try { - OperationOutcome operationOutcome = (OperationOutcome)result.getPayload(); + OperationOutcome operationOutcome = (OperationOutcome) result.getPayload(); ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation()); - return this.vread(resource.getClass(), resVersionedIdentifier.getId(),resVersionedIdentifier.getVersionId()); - } catch(ClassCastException e) { + return this.vread(resource.getClass(), resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId()); + } catch (ClassCastException e) { // if we fall throught we have the correct type already in the create } return result.getPayload(); } - public T update(Class resourceClass, T resource, String id) { - ResourceRequest result = null; - try { - List
headers = null; - result = utils.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), headers, - "Update "+resource.fhirType()+"/"+id, TIMEOUT_OPERATION); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); - } - } catch(Exception e) { - throw new EFhirClientException("An error has occurred while trying to update this resource", e); - } - // TODO oe 26.1.2015 could be made nicer if only OperationOutcome locationheader is returned with an operationOutcome would be returned (and not the resource also) we make another read - try { - OperationOutcome operationOutcome = (OperationOutcome)result.getPayload(); - ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation()); - return this.vread(resourceClass, resVersionedIdentifier.getId(),resVersionedIdentifier.getVersionId()); - } catch(ClassCastException e) { - // if we fall throught we have the correct type already in the create - } + public T update(Class resourceClass, T resource, String id) { + ResourceRequest result = null; + try { + result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id), + ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())), + getPreferredResourceFormat(),"Update " + resource.fhirType() + "/" + id, TIMEOUT_OPERATION); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); + } + } catch (Exception e) { + throw new EFhirClientException("An error has occurred while trying to update this resource", e); + } + // TODO oe 26.1.2015 could be made nicer if only OperationOutcome locationheader is returned with an operationOutcome would be returned (and not the resource also) we make another read + try { + OperationOutcome operationOutcome = (OperationOutcome) result.getPayload(); + ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation()); + return this.vread(resourceClass, resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId()); + } catch (ClassCastException e) { + // if we fall through we have the correct type already in the create + } - 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())), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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) { - boolean complex = false; - for (ParametersParameterComponent p : params.getParameter()) - complex = complex || !(p.getValue() instanceof PrimitiveType); - Parameters searchResults = null; - String ps = ""; - try { - if (!complex) - for (ParametersParameterComponent p : params.getParameter()) - if (p.getValue() instanceof PrimitiveType) - ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue())+"&"; - ResourceRequest result; - if (complex) { - result = utils.issuePostRequest(resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps), utils.getResourceAsByteArray(params, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), - "POST "+resourceClass.getName()+"/$"+name, TIMEOUT_OPERATION_LONG); - } else { - result = utils.issueGetResourceRequest(resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps), getPreferredResourceFormat(), "GET "+resourceClass.getName()+"/$"+name, TIMEOUT_OPERATION_LONG); - } - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addSuccessStatus(200);//Only one for now - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); - } - if (result.getPayload() instanceof Parameters) { - return (Parameters) result.getPayload(); - } else { - Parameters p_out = new Parameters(); - p_out.addParameter().setName("return").setResource(result.getPayload()); - return p_out; - } - } catch (Exception e) { - handleException("Error performing operation '"+name+"' with parameters " + ps, e); - } - return null; - } - - - public Bundle transaction(Bundle batch) { - Bundle transactionResult = null; - try { - transactionResult = utils.postBatchRequest(resourceAddress.getBaseServiceUri(), utils.getFeedAsByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), "transaction", TIMEOUT_NORMAL+batch.getEntry().size()); - } catch (Exception e) { - handleException("An error occurred trying to process this transaction request", e); - } - return transactionResult; - } - - @SuppressWarnings("unchecked") - public OperationOutcome validate(Class resourceClass, T resource, String id) { - ResourceRequest result = null; - try { - result = utils.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id), utils.getResourceAsByteArray(resource, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), "POST "+resourceClass.getName()+(id != null ? "/"+id : "")+"/$validate", TIMEOUT_OPERATION_LONG); - result.addErrorStatus(400);//gone - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200);//OK - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); - } - } catch(Exception e) { - handleException("An error has occurred while trying to validate this resource", e); - } - return (OperationOutcome)result.getPayload(); - } - - /* change to meta operations - - public List getAllTags() { - TagListRequest result = null; - try { - result = utils.issueGetRequestForTagList(resourceAddress.resolveGetAllTags(), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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), getPreferredResourceFormat(), 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())), getPreferredResourceFormat(), 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())), getPreferredResourceFormat(), 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())), getPreferredResourceFormat(), 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 - */ - protected void handleException(String message, Exception e) throws EFhirClientException { - if(e instanceof EFhirClientException) { - throw (EFhirClientException)e; - } else { - throw new EFhirClientException(message, e); - } - } - - /** - * Helper method to determine whether desired resource representation - * is Json or XML. - * - * @param format - * @return - */ - protected boolean isJson(String format) { - boolean isJson = false; - if(format.toLowerCase().contains("json")) { - isJson = true; - } - return isJson; - } - - public Bundle fetchFeed(String url) { - Bundle feed = null; - try { - feed = utils.issueGetFeedRequest(new URI(url), getPreferredResourceFormat()); - } catch (Exception e) { - handleException("An error has occurred while trying to retrieve history since last update",e); - } - return feed; + return result.getPayload(); } - + + public Parameters operateType(Class resourceClass, String name, Parameters params) { + boolean complex = false; + for (ParametersParameterComponent p : params.getParameter()) + complex = complex || !(p.getValue() instanceof PrimitiveType); + String ps = ""; + try { + if (!complex) + for (ParametersParameterComponent p : params.getParameter()) + if (p.getValue() instanceof PrimitiveType) + ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue()) + "&"; + ResourceRequest result; + if (complex) { + result = client.issuePostRequest(resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps), ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), + "POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG); + } else { + result = client.issueGetResourceRequest(resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps), getPreferredResourceFormat(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG); + } + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); + } + if (result.getPayload() instanceof Parameters) { + return (Parameters) result.getPayload(); + } else { + Parameters p_out = new Parameters(); + p_out.addParameter().setName("return").setResource(result.getPayload()); + return p_out; + } + } catch (Exception e) { + handleException("Error performing operation '" + name + "' with parameters " + ps, e); + } + return null; + } + + + public Bundle transaction(Bundle batch) { + Bundle transactionResult = null; + try { + transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), "transaction", TIMEOUT_NORMAL + batch.getEntry().size()); + } catch (Exception e) { + handleException("An error occurred trying to process this transaction request", e); + } + return transactionResult; + } + + @SuppressWarnings("unchecked") + public OperationOutcome validate(Class resourceClass, T resource, String id) { + ResourceRequest result = null; + try { + result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id), ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", TIMEOUT_OPERATION_LONG); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); + } + } catch (Exception e) { + handleException("An error has occurred while trying to validate this resource", e); + } + return (OperationOutcome) result.getPayload(); + } + + /** + * Helper method to prevent nesting of previously thrown EFhirClientExceptions + * + * @param e + * @throws EFhirClientException + */ + protected void handleException(String message, Exception e) throws EFhirClientException { + if (e instanceof EFhirClientException) { + throw (EFhirClientException) e; + } else { + throw new EFhirClientException(message, e); + } + } + + /** + * Helper method to determine whether desired resource representation + * is Json or XML. + * + * @param format + * @return + */ + protected boolean isJson(String format) { + boolean isJson = false; + if (format.toLowerCase().contains("json")) { + isJson = true; + } + return isJson; + } + + public Bundle fetchFeed(String url) { + Bundle feed = null; + try { + feed = client.issueGetFeedRequest(new URI(url), getPreferredResourceFormat()); + } catch (Exception e) { + handleException("An error has occurred while trying to retrieve history since last update", e); + } + return feed; + } + public ValueSet expandValueset(ValueSet source, Parameters expParams) { - 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())), getPreferredResourceFormat(), headers, "ValueSet/$expand?url="+source.getUrl(), TIMEOUT_OPERATION_EXPAND); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + org.hl7.fhir.r5.utils.client.network.ResourceRequest result = null; + try { + result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"), + ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), null, "ValueSet/$expand?url=" + source.getUrl(), TIMEOUT_OPERATION_EXPAND); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } - return (ValueSet) result.getPayload(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return result == null ? null : (ValueSet) result.getPayload(); } - + public Parameters lookupCode(Map params) { - ResourceRequest result = utils.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), getPreferredResourceFormat(), "CodeSystem/$lookup", TIMEOUT_NORMAL); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + org.hl7.fhir.r5.utils.client.network.ResourceRequest result = null; + try { + result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), getPreferredResourceFormat(), "CodeSystem/$lookup", TIMEOUT_NORMAL); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } return (Parameters) result.getPayload(); } + public ValueSet expandValueset(ValueSet source, Parameters expParams, Map params) { - List
headers = null; Parameters p = expParams == null ? new Parameters() : expParams.copy(); p.addParameter().setName("valueSet").setResource(source); for (String n : params.keySet()) { p.addParameter().setName(n).setValue(new StringType(params.get(n))); } - ResourceRequest result = utils.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params), - utils.getResourceAsByteArray(p, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), headers, "ValueSet/$expand?url="+source.getUrl(), TIMEOUT_OPERATION_EXPAND); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + org.hl7.fhir.r5.utils.client.network.ResourceRequest result = null; + try { + result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params), + ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), null, "ValueSet/$expand?url=" + source.getUrl(), TIMEOUT_OPERATION_EXPAND); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } - return (ValueSet) result.getPayload(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return result == null ? null : (ValueSet) result.getPayload(); } - -// public ValueSet expandValueset(ValueSet source, ExpansionProfile profile, Map params) { -// List
headers = null; -// ResourceRequest result = utils.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params), -// utils.getResourceAsByteArray(source, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), headers, proxy); -// result.addErrorStatus(410);//gone -// result.addErrorStatus(404);//unknown -// result.addErrorStatus(405); -// result.addErrorStatus(422);//Unprocessable Entity -// result.addSuccessStatus(200); -// result.addSuccessStatus(201); -// if(result.isUnsuccessfulRequest()) { -// throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); -// } -// return (ValueSet) result.getPayload(); -// } - - + public String getAddress() { return base; } @@ -769,79 +404,60 @@ public class FHIRToolingClient { public ConceptMap initializeClosure(String name) { 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())), getPreferredResourceFormat(), headers, "Closure?name="+name, TIMEOUT_NORMAL); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + ResourceRequest result = null; + try { + result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap()), + ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), null, "Closure?name=" + name, TIMEOUT_NORMAL); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } - return (ConceptMap) result.getPayload(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return result == null ? null : (ConceptMap) result.getPayload(); } 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())), getPreferredResourceFormat(), headers, "UpdateClosure?name="+name, TIMEOUT_OPERATION); - result.addErrorStatus(410);//gone - result.addErrorStatus(404);//unknown - result.addErrorStatus(405); - result.addErrorStatus(422);//Unprocessable Entity - result.addSuccessStatus(200); - result.addSuccessStatus(201); - if(result.isUnsuccessfulRequest()) { - throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome)result.getPayload()); + org.hl7.fhir.r5.utils.client.network.ResourceRequest result = null; + try { + result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap()), + ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), null, "UpdateClosure?name=" + name, TIMEOUT_OPERATION); + if (result.isUnsuccessfulRequest()) { + throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); } - return (ConceptMap) result.getPayload(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return result == null ? null : (ConceptMap) result.getPayload(); } - public int getTimeout() { - return utils.getTimeout(); + public long getTimeout() { + return client.getTimeout(); } - 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 void setTimeout(long timeout) { + client.setTimeout(timeout); } public ToolingClientLogger getLogger() { - return utils.getLogger(); + return client.getLogger(); } public void setLogger(ToolingClientLogger logger) { - utils.setLogger(logger); + client.setLogger(logger); } public int getRetryCount() { - return utils.getRetryCount(); + return client.getRetryCount(); } public void setRetryCount(int retryCount) { - utils.setRetryCount(retryCount); + client.setRetryCount(retryCount); } -} \ No newline at end of file +} + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceAddress.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceAddress.java index 19c946461..2085c5bb6 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceAddress.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceAddress.java @@ -228,9 +228,6 @@ public class ResourceAddress { /** * For now, assume this type of location header structure. * Generalize later: http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1 - * - * @param serviceBase - * @param locationHeader */ public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) { Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceRequest.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceRequest.java deleted file mode 100644 index ca56224a9..000000000 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/ResourceRequest.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.hl7.fhir.r5.utils.client; - - - - - -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - -*/ - - -import java.util.ArrayList; -import java.util.List; - -import org.hl7.fhir.r5.model.Resource; - -public class ResourceRequest { - private T payload; - private int httpStatus = -1; - private String location; - private List successfulStatuses = new ArrayList(); - private List errorStatuses = new ArrayList(); - - public ResourceRequest(T payload, int httpStatus, List successfulStatuses, List errorStatuses, String location) { - this.payload = payload; - this.httpStatus = httpStatus; - if(successfulStatuses != null) { - this.successfulStatuses.addAll(successfulStatuses); - } - if(errorStatuses != null) { - this.errorStatuses.addAll(errorStatuses); - } - this.location = location; - } - - public ResourceRequest(T payload, int httpStatus, String location) { - this.payload = payload; - this.httpStatus = httpStatus; - this.location = location; - } - - public ResourceRequest(T payload, int httpStatus, int successfulStatus, String location) { - this.payload = payload; - this.httpStatus = httpStatus; - this.successfulStatuses.add(successfulStatus); - this.location = location; - } - - public int getHttpStatus() { - return httpStatus; - } - - public T getPayload() { - return payload; - } - - public T getReference() { - T payloadResource = null; - if(payload != null) { - payloadResource = payload; - } - return payloadResource; - } - - public boolean isSuccessfulRequest() { - return successfulStatuses.contains(httpStatus) && !errorStatuses.contains(httpStatus) && httpStatus > 0; - } - - public boolean isUnsuccessfulRequest() { - return !isSuccessfulRequest(); - } - - public void addSuccessStatus(int status) { - this.successfulStatuses.add(status); - } - - 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.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ByteUtils.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ByteUtils.java new file mode 100644 index 000000000..7969c184b --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ByteUtils.java @@ -0,0 +1,69 @@ +package org.hl7.fhir.r5.utils.client.network; + +import org.hl7.fhir.r5.formats.IParser; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; +import org.hl7.fhir.r5.model.Bundle; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.utils.client.EFhirClientException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class ByteUtils { + + public static byte[] resourceToByteArray(T resource, boolean pretty, boolean isJson) { + ByteArrayOutputStream baos = null; + byte[] byteArray = null; + try { + baos = new ByteArrayOutputStream(); + IParser parser = null; + if (isJson) { + parser = new JsonParser(); + } else { + parser = new XmlParser(); + } + parser.setOutputStyle(pretty ? IParser.OutputStyle.PRETTY : IParser.OutputStyle.NORMAL); + parser.compose(baos, resource); + baos.close(); + byteArray = baos.toByteArray(); + baos.close(); + } catch (Exception e) { + try { + baos.close(); + } catch (Exception ex) { + throw new EFhirClientException("Error closing output stream", ex); + } + throw new EFhirClientException("Error converting output stream to byte array", e); + } + return byteArray; + } + + public static byte[] encodeFormSubmission(Map parameters, String resourceName, Resource resource, String boundary) throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + OutputStreamWriter w = new OutputStreamWriter(b, StandardCharsets.UTF_8); + for (String name : parameters.keySet()) { + w.write("--"); + w.write(boundary); + w.write("\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"); + w.write(parameters.get(name) + "\r\n"); + } + w.write("--"); + w.write(boundary); + w.write("\r\nContent-Disposition: form-data; name=\"" + resourceName + "\"\r\n\r\n"); + w.close(); + JsonParser json = new JsonParser(); + json.setOutputStyle(IParser.OutputStyle.NORMAL); + json.compose(b, resource); + b.close(); + w = new OutputStreamWriter(b, StandardCharsets.UTF_8); + w.write("\r\n--"); + w.write(boundary); + w.write("--"); + w.close(); + return b.toByteArray(); + } +} 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 new file mode 100644 index 000000000..a41289663 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/Client.java @@ -0,0 +1,234 @@ +package org.hl7.fhir.r5.utils.client.network; + +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +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 java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URLConnection; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class Client { + + public static final String DEFAULT_CHARSET = "UTF-8"; + private static final long DEFAULT_TIMEOUT = 5000; + private String username; + private String password; + private ToolingClientLogger logger; + private int retryCount; + private long timeout = DEFAULT_TIMEOUT; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public ToolingClientLogger getLogger() { + return logger; + } + + public void setLogger(ToolingClientLogger logger) { + this.logger = logger; + } + + public int getRetryCount() { + return retryCount; + } + + public void setRetryCount(int retryCount) { + this.retryCount = retryCount; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public ResourceRequest issueOptionsRequest(URI optionsUri, + String resourceFormat, + String message, + long timeout) throws MalformedURLException { + Request.Builder request = new Request.Builder() + .method("OPTIONS", null) + .url(optionsUri.toURL()); + + return executeFhirRequest(request, resourceFormat, null, message, retryCount, timeout); + } + + public ResourceRequest issueGetResourceRequest(URI resourceUri, + String resourceFormat, + String message, + long timeout) throws MalformedURLException { + Request.Builder request = new Request.Builder() + .url(resourceUri.toURL()); + + return executeFhirRequest(request, resourceFormat, null, message, retryCount, timeout); + } + + public ResourceRequest issuePutRequest(URI resourceUri, + byte[] payload, + String resourceFormat, + String message, + long timeout) throws MalformedURLException { + return issuePutRequest(resourceUri, payload, resourceFormat, null, message, timeout); + } + + public ResourceRequest issuePutRequest(URI resourceUri, + byte[] payload, + String resourceFormat, + Headers headers, + String message, + long timeout) throws MalformedURLException { + 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); + + return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); + } + + public ResourceRequest issuePostRequest(URI resourceUri, + byte[] payload, + String resourceFormat, + String message, + long timeout) throws MalformedURLException { + return issuePostRequest(resourceUri, payload, resourceFormat, null, message, timeout); + } + + public ResourceRequest issuePostRequest(URI resourceUri, + byte[] payload, + String resourceFormat, + Headers headers, + String message, + long timeout) throws MalformedURLException { + 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 executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout); + } + + public boolean issueDeleteRequest(URI resourceUri) throws MalformedURLException { + Request.Builder request = new Request.Builder() + .url(resourceUri.toURL()) + .delete(); + return executeFhirRequest(request, null, null, null, retryCount, timeout).isSuccessfulRequest(); + } + + public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws MalformedURLException { + Request.Builder request = new Request.Builder() + .url(resourceUri.toURL()); + + return executeBundleRequest(request, resourceFormat, null, 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, null, null, retryCount, timeout); + } + + public Bundle postBatchRequest(URI resourceUri, + byte[] payload, + String resourceFormat, + String message, + int timeout) throws MalformedURLException { + 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, null, message, retryCount, timeout); + } + + protected Bundle executeBundleRequest(Request.Builder request, + String resourceFormat, + Headers headers, + String message, + int retryCount, + long timeout) { + return new FhirRequestBuilder(request) + .withLogger(logger) + .withResourceFormat(resourceFormat) + .withRetryCount(retryCount) + .withMessage(message) + .withHeaders(headers == null ? new Headers.Builder().build() : headers) + .withTimeout(timeout, TimeUnit.MILLISECONDS) + .executeAsBatch(); + } + + protected ResourceRequest executeFhirRequest(Request.Builder request, + String resourceFormat, + Headers headers, + String message, + int retryCount, + long timeout) { + return new FhirRequestBuilder(request) + .withLogger(logger) + .withResourceFormat(resourceFormat) + .withRetryCount(retryCount) + .withMessage(message) + .withHeaders(headers == null ? new Headers.Builder().build() : headers) + .withTimeout(timeout, TimeUnit.MILLISECONDS) + .execute(); + } + + /** + * @deprecated It does not appear as though this method is actually being used. Will be removed in a future release + * unless a case is made to keep it. + */ + @Deprecated + public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) { + String dateTime = null; + try { + dateTime = serverConnection.getHeaderField("Last-Modified"); + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", new Locale("en", "US")); + Date lastModifiedTimestamp = format.parse(dateTime); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(lastModifiedTimestamp); + return calendar; + } catch (ParseException pe) { + throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe); + } + } +} 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 new file mode 100644 index 000000000..46cadea91 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilder.java @@ -0,0 +1,339 @@ +package org.hl7.fhir.r5.utils.client.network; + +import kotlin.Pair; +import okhttp3.*; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r5.formats.IParser; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; +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.ResourceUtilities; +import org.hl7.fhir.r5.utils.client.EFhirClientException; +import org.hl7.fhir.r5.utils.client.ResourceFormat; +import org.hl7.fhir.utilities.ToolingClientLogger; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +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 String resourceFormat = null; + private Headers headers = null; + private String message = null; + private int retryCount = 1; + /** + * The timeout quantity. Used in combination with {@link FhirRequestBuilder#timeoutUnit}. + */ + private long timeout = 5000; + /** + * Time unit for {@link FhirRequestBuilder#timeout}. + */ + private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; + /** + * {@link ToolingClientLogger} for log output. + */ + private ToolingClientLogger logger = null; + + public FhirRequestBuilder(Request.Builder httpRequest) { + this.httpRequest = httpRequest; + } + + /** + * Adds necessary default headers, formatting headers, and any passed in {@link Headers} 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); + 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
  • + *
  • Accept-Charset : {@link FhirRequestBuilder#DEFAULT_CHARSET}
  • + * + * @param request {@link Request.Builder} to add default headers to. + */ + protected static void addDefaultHeaders(Request.Builder request) { + request.addHeader("User-Agent", "hapi-fhir-tooling-client"); + request.addHeader("Accept-Charset", DEFAULT_CHARSET); + } + + /** + * Adds necessary headers for the given resource format provided. + * + * @param request {@link Request.Builder} to add default headers to. + */ + protected static void addResourceFormatHeaders(Request.Builder request, String format) { + request.addHeader("Accept", format); + request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); + } + + /** + * Iterates through the passed in {@link Headers} and adds them to the provided {@link Request.Builder}. + * + * @param request {@link Request.Builder} to add headers to. + * @param headers {@link Headers} to add to request. + */ + protected static void addHeaders(Request.Builder request, Headers headers) { + headers.forEach(header -> request.addHeader(header.getFirst(), header.getSecond())); + } + + /** + * 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} + * + * @param oo {@link OperationOutcome} to evaluate. + * @return {@link Boolean#TRUE} if an error exists. + */ + protected static boolean hasError(OperationOutcome oo) { + return (oo.getIssue().stream() + .anyMatch(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR + || 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 (okHttpClient == null) { + okHttpClient = new OkHttpClient(); + } + + Authenticator proxyAuthenticator = (route, response) -> { + String credential = Credentials.basic(System.getProperty(HTTP_PROXY_USER), System.getProperty(HTTP_PROXY_PASS)); + return response.request().newBuilder() + .header(HEADER_PROXY_AUTH, credential) + .build(); + }; + + return okHttpClient.newBuilder() + .addInterceptor(new RetryInterceptor(retryCount)) + .connectTimeout(timeout, timeoutUnit) + .writeTimeout(timeout, timeoutUnit) + .readTimeout(timeout, timeoutUnit) + .proxyAuthenticator(proxyAuthenticator) + .build(); + } + + public FhirRequestBuilder withResourceFormat(String resourceFormat) { + this.resourceFormat = resourceFormat; + return this; + } + + public FhirRequestBuilder withHeaders(Headers headers) { + this.headers = headers; + return this; + } + + public FhirRequestBuilder withMessage(String message) { + this.message = message; + return this; + } + + public FhirRequestBuilder withRetryCount(int retryCount) { + this.retryCount = retryCount; + return this; + } + + public FhirRequestBuilder withLogger(ToolingClientLogger logger) { + this.logger = logger; + return this; + } + + public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) { + this.timeout = timeout; + this.timeoutUnit = unit; + return this; + } + + protected Request buildRequest() { + return httpRequest.build(); + } + + public ResourceRequest execute() { + formatHeaders(httpRequest, resourceFormat, null); + + try { + Response response = getHttpClient().newCall(httpRequest.build()).execute(); + T resource = unmarshalReference(response, resourceFormat); + return new ResourceRequest(resource, response.code(), getLocationHeader(response.headers())); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + public Bundle executeAsBatch() { + formatHeaders(httpRequest, resourceFormat, null); + + try { + Response response = getHttpClient().newCall(httpRequest.build()).execute(); + return unmarshalFeed(response, resourceFormat); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + /** + * Unmarshalls a resource from the response stream. + */ + @SuppressWarnings("unchecked") + protected T unmarshalReference(Response response, String format) { + T resource = null; + OperationOutcome error = null; + + if (response.body() != null) { + try { + byte[] body = response.body().bytes(); + log(response.code(), response.headers(), body); + resource = (T) getParser(format).parse(body); + if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) { + error = (OperationOutcome) resource; + } + } catch (IOException ioe) { + throw new EFhirClientException("Error reading Http Response: " + ioe.getMessage(), ioe); + } catch (Exception e) { + throw new EFhirClientException("Error parsing response message: " + e.getMessage(), e); + } + } + + if (error != null) { + throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error); + } + + return resource; + } + + /** + * Unmarshalls Bundle from response stream. + */ + protected Bundle unmarshalFeed(Response 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"); + if (body != null) { + if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) { + Resource rf = getParser(format).parse(body); + if (rf instanceof Bundle) + feed = (Bundle) rf; + else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { + error = (OperationOutcome) rf; + } else { + throw new EFhirClientException("Error reading server response: a resource was returned instead"); + } + } + } + } catch (IOException ioe) { + throw new EFhirClientException("Error reading Http Response", ioe); + } catch (Exception e) { + throw new EFhirClientException("Error parsing response message", e); + } + if (error != null) { + throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error); + } + return feed; + } + + /** + * 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". + * @return {@link JsonParser} or {@link XmlParser} + */ + protected IParser getParser(String format) { + if (StringUtils.isBlank(format)) { + format = ResourceFormat.RESOURCE_XML.getHeader(); + } + if (format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) { + return new JsonParser(); + } else if (format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) { + return new XmlParser(); + } else { + throw new EFhirClientException("Invalid format: " + format); + } + } + + /** + * 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 { + logger.logResponse(Integer.toString(responseCode), headerList, responseBody); + } catch (Exception e) { + System.out.println("Error parsing response body passed in to logger ->\n" + e.getLocalizedMessage()); + } + } else { + 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.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ResourceRequest.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ResourceRequest.java new file mode 100644 index 000000000..7c6abdaf6 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/ResourceRequest.java @@ -0,0 +1,71 @@ +package org.hl7.fhir.r5.utils.client.network; + +/* + Copyright (c) 2011+, HL7, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of HL7 nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +import org.hl7.fhir.r5.model.Resource; + +public class ResourceRequest { + private T payload; + private int httpStatus = -1; + private String location; + + public ResourceRequest(T payload, int httpStatus, String location) { + this.payload = payload; + this.httpStatus = httpStatus; + this.location = location; + } + + public int getHttpStatus() { + return httpStatus; + } + + public T getPayload() { + return payload; + } + + public T getReference() { + T payloadResource = null; + if (payload != null) { + payloadResource = payload; + } + return payloadResource; + } + + public boolean isSuccessfulRequest() { + return this.httpStatus / 100 == 2; + } + + public boolean isUnsuccessfulRequest() { + return !isSuccessfulRequest(); + } + + public String getLocation() { + return location; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..5e5d37871 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/client/network/RetryInterceptor.java @@ -0,0 +1,63 @@ +package org.hl7.fhir.r5.utils.client.network; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +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 @NotNull 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/network/ClientTest.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientTest.java new file mode 100644 index 000000000..01a13a870 --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/ClientTest.java @@ -0,0 +1,146 @@ +package org.hl7.fhir.r5.utils.client.network; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.model.*; +import org.junit.jupiter.api.*; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ClientTest { + + private static final long TIMEOUT = 5000; + + private MockWebServer server; + private HttpUrl serverUrl; + private Client client; + + private Address address = new Address() + .setCity("Toronto") + .setState("Ontario") + .setCountry("Canada"); + private HumanName humanName = new HumanName() + .addGiven("Mark") + .setFamily("Iantorno"); + private 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, 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, 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, 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); + // 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); + // 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."); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..24cf3b21b --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/utils/client/network/FhirRequestBuilderTest.java @@ -0,0 +1,147 @@ +package org.hl7.fhir.r5.utils.client.network; + +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.hl7.fhir.r5.model.OperationOutcome; +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; + +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); + + 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\"."); + + Assertions.assertNotNull(headersMap.get("Accept-Charset"), "Accept-Charset header null."); + Assertions.assertEquals(FhirRequestBuilder.DEFAULT_CHARSET, headersMap.get("Accept-Charset").get(0), + "Accept-Charset header not populated with expected value " + FhirRequestBuilder.DEFAULT_CHARSET); + } + + @Test + @DisplayName("Test resource format headers are added correctly.") + void addResourceFormatHeaders() { + String testFormat = "yaml"; + Request.Builder request = new Request.Builder().url("http://www.google.com"); + FhirRequestBuilder.addResourceFormatHeaders(request, testFormat); + + Map> headersMap = request.build().headers().toMultimap(); + 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 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() { + 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"; + Headers headers = new Headers.Builder() + .add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader) + .build(); + 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"; + Headers headers = new Headers.Builder() + .add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) + .build(); + 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"; + Headers headers = new Headers.Builder() + .add(FhirRequestBuilder.LOCATION_HEADER, expectedLocationHeader) + .add(FhirRequestBuilder.CONTENT_LOCATION_HEADER, expectedContentLocationHeader) + .build(); + 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)); + } +} \ 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 45dd6f34b..9a59e85c7 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 @@ -68,6 +68,11 @@ import org.hl7.fhir.validation.cli.services.ValidationService; import org.hl7.fhir.validation.cli.utils.*; import java.io.File; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.net.URLConnection; +import java.util.Base64; /** * A executable class that will validate one or more FHIR resources against @@ -86,6 +91,11 @@ public class ValidatorCli { public static final String HTTP_PROXY_HOST = "http.proxyHost"; public static final String HTTP_PROXY_PORT = "http.proxyPort"; + public static final String HTTP_PROXY_USER = "http.proxyUser"; + public static final String HTTP_PROXY_PASS = "http.proxyPassword"; + public static final String JAVA_DISABLED_TUNNELING_SCHEMES = "jdk.http.auth.tunneling.disabledSchemes"; + public static final String JAVA_DISABLED_PROXY_SCHEMES = "jdk.http.auth.proxying.disabledSchemes"; + public static final String JAVA_USE_SYSTEM_PROXIES = "java.net.useSystemProxies"; public static void main(String[] args) throws Exception { TimeTracker tt = new TimeTracker(); @@ -95,11 +105,45 @@ public class ValidatorCli { Display.displaySystemInfo(); if (Params.hasParam(args, Params.PROXY)) { - String[] p = Params.getParam(args, Params.PROXY).split("\\:"); + assert Params.getParam(args, Params.PROXY) != null : "PROXY arg passed in was NULL"; + String[] p = Params.getParam(args, Params.PROXY).split(":"); System.setProperty(HTTP_PROXY_HOST, p[0]); System.setProperty(HTTP_PROXY_PORT, p[1]); } + if (Params.hasParam(args, Params.PROXY_AUTH)) { + assert Params.getParam(args, Params.PROXY) != null : "Cannot set PROXY_AUTH without setting PROXY..."; + assert Params.getParam(args, Params.PROXY_AUTH) != null : "PROXY_AUTH arg passed in was NULL..."; + String[] p = Params.getParam(args, Params.PROXY_AUTH).split(":"); + String authUser = p[0]; + String authPass = p[1]; + + /* + * For authentication, use java.net.Authenticator to set proxy's configuration and set the system properties + * http.proxyUser and http.proxyPassword + */ + Authenticator.setDefault( + new Authenticator() { + @Override + public PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(authUser, authPass.toCharArray()); + } + } + ); + + System.setProperty(HTTP_PROXY_USER, authUser); + System.setProperty(HTTP_PROXY_PASS, authPass); + System.setProperty(JAVA_USE_SYSTEM_PROXIES, "true"); + + /* + * For Java 1.8 and higher you must set + * -Djdk.http.auth.tunneling.disabledSchemes= + * to make proxies with Basic Authorization working with https along with Authenticator + */ + System.setProperty(JAVA_DISABLED_TUNNELING_SCHEMES, ""); + System.setProperty(JAVA_DISABLED_PROXY_SCHEMES, ""); + } + CliContext cliContext = Params.loadCliContext(args); if (Params.hasParam(args, Params.TEST)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java index 51fdfc99d..fcb40d1f6 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java @@ -1,5 +1,6 @@ package org.hl7.fhir.validation.cli.utils; +import org.apache.http.auth.AUTH; import org.hl7.fhir.r5.utils.IResourceValidator.BundleValidationRule; import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.validation.cli.model.CliContext; @@ -14,6 +15,7 @@ public class Params { public static final String OUTPUT = "-output"; public static final String HTML_OUTPUT = "-html-output"; public static final String PROXY = "-proxy"; + public static final String PROXY_AUTH = "-auth"; public static final String PROFILE = "-profile"; public static final String BUNDLE = "-bundle"; public static final String QUESTIONNAIRE = "-questionnaire"; @@ -101,6 +103,8 @@ public class Params { cliContext.setHtmlOutput(args[++i]); } else if (args[i].equals(PROXY)) { i++; // ignore next parameter + } else if (args[i].equals(PROXY_AUTH)) { + i++; } else if (args[i].equals(PROFILE)) { String p = null; if (i + 1 == args.length) {