mirror of
https://github.com/hapifhir/org.hl7.fhir.core.git
synced 2025-02-09 14:24:44 +00:00
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
This commit is contained in:
parent
9ffa3f7015
commit
7e5a670846
@ -101,6 +101,11 @@
|
||||
<artifactId>httpclient</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.9.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
@ -119,6 +124,13 @@
|
||||
<version>${validator_test_case_version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>4.9.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- JUnit Jupiter -->
|
||||
<dependency>
|
||||
|
@ -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 <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat, String message, int timeout) {
|
||||
HttpOptions options = new HttpOptions(optionsUri);
|
||||
return issueResourceRequest(resourceFormat, options, message, timeout);
|
||||
}
|
||||
|
||||
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat, String message, int timeout) {
|
||||
HttpGet httpget = new HttpGet(resourceUri);
|
||||
return issueResourceRequest(resourceFormat, httpget, message, timeout);
|
||||
}
|
||||
|
||||
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers, String message, int timeout) {
|
||||
HttpPut httpPut = new HttpPut(resourceUri);
|
||||
return issueResourceRequest(resourceFormat, httpPut, payload, headers, message, timeout);
|
||||
}
|
||||
|
||||
public <T extends Resource> ResourceRequest<T> 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 <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers, String message, int timeout) {
|
||||
HttpPost httpPost = new HttpPost(resourceUri);
|
||||
return issueResourceRequest(resourceFormat, httpPost, payload, headers, message, timeout);
|
||||
}
|
||||
|
||||
|
||||
public <T extends Resource> ResourceRequest<T> 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 <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, String message, int timeout) {
|
||||
return issueResourceRequest(resourceFormat, request, null, message, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resourceFormat
|
||||
* @param options
|
||||
* @return
|
||||
*/
|
||||
protected <T extends Resource> ResourceRequest<T> 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 <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List<Header> 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<T>(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<Header> 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 extends Resource> 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 <T extends Resource> 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<String, String> 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<String, String> 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<String> 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<String> 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<String> 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
@ -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<T extends Resource> {
|
||||
private T payload;
|
||||
private int httpStatus = -1;
|
||||
private String location;
|
||||
private List<Integer> successfulStatuses = new ArrayList<Integer>();
|
||||
private List<Integer> errorStatuses = new ArrayList<Integer>();
|
||||
|
||||
public ResourceRequest(T payload, int httpStatus, List<Integer> successfulStatuses, List<Integer> 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;
|
||||
}
|
||||
}
|
@ -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 <T extends Resource> 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<String, String> 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();
|
||||
}
|
||||
}
|
@ -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 <T extends Resource> ResourceRequest<T> 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 <T extends Resource> ResourceRequest<T> 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 <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
|
||||
byte[] payload,
|
||||
String resourceFormat,
|
||||
String message,
|
||||
long timeout) throws MalformedURLException {
|
||||
return issuePutRequest(resourceUri, payload, resourceFormat, null, message, timeout);
|
||||
}
|
||||
|
||||
public <T extends Resource> ResourceRequest<T> 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 <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
|
||||
byte[] payload,
|
||||
String resourceFormat,
|
||||
String message,
|
||||
long timeout) throws MalformedURLException {
|
||||
return issuePostRequest(resourceUri, payload, resourceFormat, null, message, timeout);
|
||||
}
|
||||
|
||||
public <T extends Resource> ResourceRequest<T> 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<String, String> 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 <T extends Resource> 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 <T extends Resource> ResourceRequest<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <li>User-Agent : hapi-fhir-tooling-client</li>
|
||||
* <li>Accept-Charset : {@link FhirRequestBuilder#DEFAULT_CHARSET}</li>
|
||||
*
|
||||
* @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<String, List<String>> headerMap = headers.toMultimap();
|
||||
if (headerMap.containsKey(LOCATION_HEADER)) {
|
||||
return headerMap.get(LOCATION_HEADER).get(0);
|
||||
} else if (headerMap.containsKey(CONTENT_LOCATION_HEADER)) {
|
||||
return headerMap.get(CONTENT_LOCATION_HEADER).get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only ever want to have one copy of the HttpClient kicking around at any given time. If we need to make changes
|
||||
* to any configuration, such as proxy settings, timeout, caches, etc, we can do a per-call configuration through
|
||||
* the {@link OkHttpClient#newBuilder()} method. That will return a builder that shares the same connection pool,
|
||||
* dispatcher, and configuration with the original client.
|
||||
* </p>
|
||||
* The {@link OkHttpClient} uses the proxy auth properties set in the current system properties. The reason we don't
|
||||
* set the proxy address and authentication explicitly, is due to the fact that this class is often used in conjunction
|
||||
* with other http client tools which rely on the system.properties settings to determine proxy settings. It's easier
|
||||
* to keep the method consistent across the board. ...for now.
|
||||
*
|
||||
* @return {@link OkHttpClient} instance
|
||||
*/
|
||||
protected OkHttpClient getHttpClient() {
|
||||
if (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 <T extends Resource> ResourceRequest<T> execute() {
|
||||
formatHeaders(httpRequest, resourceFormat, null);
|
||||
|
||||
try {
|
||||
Response response = getHttpClient().newCall(httpRequest.build()).execute();
|
||||
T resource = unmarshalReference(response, resourceFormat);
|
||||
return new ResourceRequest<T>(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 extends Resource> 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.
|
||||
* <p>
|
||||
* 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<String> headerList = new ArrayList<>(Collections.emptyList());
|
||||
Map<String, List<String>> headerMap = responseHeaders.toMultimap();
|
||||
headerMap.keySet().forEach(key -> headerMap.get(key).forEach(value -> headerList.add(key + ":" + value)));
|
||||
|
||||
try {
|
||||
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?");
|
||||
}
|
||||
}
|
||||
}
|
@ -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<T extends Resource> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<Resource> 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<Resource> 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<Resource> 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<Resource> request = client.issuePutRequest(new URI(serverUrl.toString()), payload,
|
||||
"xml", null, TIMEOUT);
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
Assertions.assertArrayEquals(payload, recordedRequest.getBody().readByteArray(),
|
||||
"PUT request payload does not match send data.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST request, test payload received by server matches sent.")
|
||||
void test_post() throws IOException, URISyntaxException, InterruptedException {
|
||||
byte[] payload = ByteUtils.resourceToByteArray(patient, true, false);
|
||||
// Mock server response of 200, with the same resource payload returned that we included in the PUT request
|
||||
server.enqueue(
|
||||
new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody(new String(payload))
|
||||
);
|
||||
|
||||
ResourceRequest<Resource> request = client.issuePostRequest(new URI(serverUrl.toString()), payload,
|
||||
"xml", null, TIMEOUT);
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
Assertions.assertArrayEquals(payload, recordedRequest.getBody().readByteArray(),
|
||||
"POST request payload does not match send data.");
|
||||
}
|
||||
}
|
@ -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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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));
|
||||
}
|
||||
}
|
@ -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)) {
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user