Terminology client headers (#599)

* wip

* adding tests for headers in client

* fix

* r4 updated httpclient

* updating http client code for r4 and dstu3

* dunno why this didn't get added before
This commit is contained in:
Mark Iantorno 2021-09-09 16:32:19 -04:00 committed by GitHub
parent 7a989a835c
commit e5a05f5562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 3270 additions and 3303 deletions

View File

@ -21,7 +21,6 @@ public class VSACImporter extends OIDBasedValueSetImporter {
}
public static void main(String[] args) throws FHIRException, IOException, ParseException, URISyntaxException {
// new PhinVadsImporter().importValueSet(TextFile.fileToBytes("C:\\work\\org.hl7.fhir\\packages\\us.cdc.phinvads-source\\source\\PHVS_BirthDefectsLateralityatDiagnosis_HL7_V1.txt"));
VSACImporter self = new VSACImporter();
self.process(args[0], args[1], args[2], args[3]);
}
@ -29,12 +28,16 @@ public class VSACImporter extends OIDBasedValueSetImporter {
private void process(String source, String dest, String username, String password) throws FHIRException, IOException, URISyntaxException {
CSVReader csv = new CSVReader(new FileInputStream(source));
csv.readHeaders();
FHIRToolingClient client = new FHIRToolingClient("https://cts.nlm.nih.gov/fhir", username, password);
FHIRToolingClient fhirToolingClient = new FHIRToolingClient("https://cts.nlm.nih.gov/fhir");
fhirToolingClient.setUsername(username);
fhirToolingClient.setPassword(password);
int i = 0;
while (csv.line()) {
String oid = csv.cell("OID");
try {
ValueSet vs = client.read(ValueSet.class, oid);
ValueSet vs = fhirToolingClient.read(ValueSet.class, oid);
new JsonParser().compose(new FileOutputStream(Utilities.path(dest, "ValueSet-" + oid + ".json")), vs);
i++;
if (i % 100 == 0) {
@ -45,68 +48,5 @@ public class VSACImporter extends OIDBasedValueSetImporter {
}
}
System.out.println("Done. " + i + " ValueSets");
// for (File f : new File(source).listFiles()) {
// try {
// System.out.println("Process " + f.getName());
// List<ValueSet> vsl = importValueSet(TextFile.fileToBytes(f));
// for (ValueSet vs : vsl) {
// if (vs.getId() != null) {
// new JsonParser().compose(new FileOutputStream(Utilities.path(dest, "ValueSet-" + vs.getId() + ".json")), vs);
// }
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
}
// private List<ValueSet> importValueSet(byte[] source) throws Exception {
// List<ValueSet> res = new ArrayList<ValueSet>();
// Element x = loadXml(new ByteArrayInputStream(source)).getDocumentElement();
// List<Element> vl = XMLUtil.getNamedChildren(x, "DescribedValueSet");
// for (Element v : vl) {
// ValueSet vs = new ValueSet();
// vs.setId(v.getAttribute("ID"));
// vs.setUrl("http://cts.nlm.nih.gov/fhir/ValueSet/" + vs.getId());
// vs.getMeta().setSource("https://vsac.nlm.nih.gov/valueset/" + vs.getId() + "/expansion");
// vs.setVersion(v.getAttribute("version"));
// vs.setTitle(v.getAttribute("displayName"));
// vs.setName(Utilities.titleize(vs.getTitle()).replace(" ", ""));
// Element d = XMLUtil.getNamedChild(v, "Purpose");
// if (d != null) {
// vs.setDescription(d.getTextContent());
// }
// Element s = XMLUtil.getNamedChild(v, "Status");
// if (s != null && "Active".equals(s.getTextContent())) {
// vs.setStatus(PublicationStatus.ACTIVE);
// } else {
// vs.setStatus(PublicationStatus.DRAFT);
// }
// Element dt = XMLUtil.getNamedChild(v, "RevisionDate");
// if (dt != null) {
// vs.getDateElement().setValueAsString(dt.getTextContent());
// }
//
// Element cl = XMLUtil.getNamedChild(v, "ConceptList");
// Element cc = XMLUtil.getFirstChild(cl);
//
// while (cc != null) {
// String code = cc.getAttribute("code");
// String display = cc.getAttribute("displayName");
// String csoid = cc.getAttribute("codeSystem");
// String csver = cc.getAttribute("codeSystemVersion");
// String url = context.oid2Uri(csoid);
// if (url == null) {
// url = "urn:oid:" + csoid;
// }
// csver = fixVersionforSystem(url, csver);
// ConceptSetComponent inc = getInclude(vs, url, csver);
// inc.addConcept().setCode(code).setDisplay(display);
// cc = XMLUtil.getNextSibling(cc);
// }
//
// res.add(vs);
// }
// return res;
// }
}

View File

@ -37,6 +37,7 @@ import org.hl7.fhir.dstu2.utils.client.FHIRToolingClient;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
@ -143,5 +144,13 @@ public class TerminologyClientR2 implements TerminologyClient {
return (CanonicalResource) r5;
}
@Override
public ClientHeaders getClientHeaders() {
return null;
}
@Override
public TerminologyClient setClientHeaders(ClientHeaders clientHeaders) {
return null;
}
}

View File

@ -37,6 +37,7 @@ import org.hl7.fhir.dstu3.utils.client.FHIRToolingClient;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
@ -46,9 +47,16 @@ import java.util.Map;
public class TerminologyClientR3 implements TerminologyClient {
private final FHIRToolingClient client; // todo: use the R2 client
private ClientHeaders clientHeaders;
public TerminologyClientR3(String address) throws URISyntaxException {
client = new FHIRToolingClient(address);
setClientHeaders(new ClientHeaders());
}
public TerminologyClientR3(String address, ClientHeaders clientHeaders) throws URISyntaxException {
client = new FHIRToolingClient(address);
setClientHeaders(clientHeaders);
}
@Override
@ -143,5 +151,15 @@ public class TerminologyClientR3 implements TerminologyClient {
return (CanonicalResource) r5;
}
@Override
public ClientHeaders getClientHeaders() {
return clientHeaders;
}
@Override
public TerminologyClient setClientHeaders(ClientHeaders clientHeaders) {
this.clientHeaders = clientHeaders;
this.client.setClientHeaders(this.clientHeaders.headers());
return this;
}
}

View File

@ -37,6 +37,7 @@ import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
@ -46,9 +47,16 @@ import java.util.Map;
public class TerminologyClientR4 implements TerminologyClient {
private final FHIRToolingClient client; // todo: use the R2 client
private ClientHeaders clientHeaders;
public TerminologyClientR4(String address) throws URISyntaxException {
client = new FHIRToolingClient(address);
this.client = new FHIRToolingClient(address);
setClientHeaders(new ClientHeaders());
}
public TerminologyClientR4(String address, ClientHeaders clientHeaders) throws URISyntaxException {
this.client = new FHIRToolingClient(address);
setClientHeaders(clientHeaders);
}
@Override
@ -143,4 +151,15 @@ public class TerminologyClientR4 implements TerminologyClient {
return (CanonicalResource) r5;
}
@Override
public ClientHeaders getClientHeaders() {
return clientHeaders;
}
@Override
public TerminologyClient setClientHeaders(ClientHeaders clientHeaders) {
this.clientHeaders = clientHeaders;
this.client.setClientHeaders(this.clientHeaders.headers());
return this;
}
}

View File

@ -34,6 +34,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.terminologies.TerminologyClient;
import org.hl7.fhir.r5.utils.client.FHIRToolingClient;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
@ -43,9 +44,16 @@ import java.util.Map;
public class TerminologyClientR5 implements TerminologyClient {
private final FHIRToolingClient client;
private ClientHeaders clientHeaders;
public TerminologyClientR5(String address) throws URISyntaxException {
client = new FHIRToolingClient(address);
this.client = new FHIRToolingClient(address);
setClientHeaders(new ClientHeaders());
}
public TerminologyClientR5(String address, ClientHeaders clientHeaders) throws URISyntaxException {
this.client = new FHIRToolingClient(address);
setClientHeaders(clientHeaders);
}
@Override
@ -129,4 +137,15 @@ public class TerminologyClientR5 implements TerminologyClient {
return (CanonicalResource) r5;
}
@Override
public ClientHeaders getClientHeaders() {
return clientHeaders;
}
@Override
public TerminologyClient setClientHeaders(ClientHeaders clientHeaders) {
this.clientHeaders = clientHeaders;
this.client.setClientHeaders(this.clientHeaders.headers());
return this;
}
}

View File

@ -51,6 +51,19 @@
<optional>true</optional>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
<optional>true</optional>
</dependency>
<!-- Apache POI -->
<dependency>
<groupId>org.apache.poi</groupId>

View File

@ -1,33 +1,33 @@
package org.hl7.fhir.dstu3.context;
/*
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.
*/
/*
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.
*/

View File

@ -1,683 +0,0 @@
package org.hl7.fhir.dstu3.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.dstu3.formats.IParser;
import org.hl7.fhir.dstu3.formats.IParser.OutputStyle;
import org.hl7.fhir.dstu3.formats.JsonParser;
import org.hl7.fhir.dstu3.formats.XmlParser;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.dstu3.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.ResourceType;
import org.hl7.fhir.dstu3.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", "Java FHIR Client for FHIR");
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 > 1) {
System.out.println("Giving up: "+ioe.getMessage()+" (R3 / "+(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;
}
}

View File

@ -33,11 +33,11 @@ package org.hl7.fhir.dstu3.utils.client;
*/
import org.hl7.fhir.dstu3.model.OperationOutcome;
import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.dstu3.model.OperationOutcome;
/**
* FHIR client exception.
*
@ -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) {

View File

@ -34,26 +34,21 @@ package org.hl7.fhir.dstu3.utils.client;
*/
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.ResourceType;
import org.hl7.fhir.utilities.Utilities;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//Make resources address subclass of URI
/**
* Helper class to manage FHIR Resource URIs
*
@ -217,23 +212,19 @@ public class ResourceAddress {
return res;
}
public URI resolveMetadataUri(boolean quick) {
return baseServiceUri.resolve(quick ? "metadata?_summary=true" : "metadata");
}
public URI resolveMetadataUri(boolean quick) {
return baseServiceUri.resolve(quick ? "metadata?_summary=true" : "metadata");
}
public URI resolveMetadataTxCaps() {
return baseServiceUri.resolve("metadata?mode=terminology");
}
/**
* 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) {
public static ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
Matcher matcher = pattern.matcher(locationResponseHeader);
ResourceVersionedIdentifier parsedHeader = null;
@ -430,5 +421,4 @@ public class ResourceAddress {
}
}
}

View File

@ -1,109 +0,0 @@
package org.hl7.fhir.dstu3.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.dstu3.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;
}
}

View File

@ -0,0 +1,68 @@
package org.hl7.fhir.dstu3.utils.client.network;
import org.hl7.fhir.dstu3.formats.IParser;
import org.hl7.fhir.dstu3.formats.JsonParser;
import org.hl7.fhir.dstu3.formats.XmlParser;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.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();
}
}

View File

@ -0,0 +1,194 @@
package org.hl7.fhir.dstu3.utils.client.network;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.io.IOException;
import java.net.URI;
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 ToolingClientLogger logger;
private int retryCount;
private long timeout = DEFAULT_TIMEOUT;
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 IOException {
Request.Builder request = new Request.Builder()
.method("OPTIONS", null)
.url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public int tester(int trytry) {
return 5;
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("PUT requests require a non-null payload");
RequestBody body = RequestBody.create(payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.put(body);
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 IOException {
return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public boolean issueDeleteRequest(URI resourceUri) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.delete();
return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest();
}
public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout);
}
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, new Headers.Builder().build(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
int timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
}
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
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();
}
public <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
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();
}
}

View File

@ -0,0 +1,95 @@
package org.hl7.fhir.dstu3.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Generic Implementation of Client Headers.
*
* Stores a list of headers for HTTP calls to the TX server. Users can implement their own instance if they desire
* specific, custom behavior.
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
}
public ArrayList<Header> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", "
+ header.value + ">.");
}
headers.add(header);
return this;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
/**
* Removes the passed in header from the list of stored headers.
* @param header {@link Header} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", "
+ header.value + ">, from GenericClientHeaders that is not currently stored.");
}
return this;
}
/**
* Removes the passed in headers from the list of stored headers.
* @param headerList {@link List} of {@link Header} to remove.
* @throws FHIRException if any of the headers passed in does not exist within the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
return this;
}
@Override
public String toString() {
return this.headers.stream()
.map(header -> "\t" + header.name + ":" + header.value)
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -0,0 +1,328 @@
package org.hl7.fhir.dstu3.utils.client.network;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.formats.IParser;
import org.hl7.fhir.dstu3.formats.JsonParser;
import org.hl7.fhir.dstu3.formats.XmlParser;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.utils.ResourceUtilities;
import org.hl7.fhir.dstu3.utils.client.EFhirClientException;
import org.hl7.fhir.dstu3.utils.client.ResourceFormat;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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.dstu3.model.OperationOutcome.OperationOutcomeIssueComponent} within the
* provided {@link OperationOutcome} have an {@link org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity} of
* {@link org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity#ERROR} or
* {@link org.hl7.fhir.dstu3.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() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
T resource = unmarshalReference(response, resourceFormat);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
return unmarshalFeed(response, resourceFormat);
}
/**
* 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 { // TODO fix logs
// System.out.println("Call to log HTTP response with null ToolingClientLogger set... are you forgetting to " +
// "initialize your logger?");
// }
}
}

View File

@ -0,0 +1,71 @@
package org.hl7.fhir.dstu3.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.dstu3.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;
}
}

View File

@ -0,0 +1,62 @@
package org.hl7.fhir.dstu3.utils.client.network;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
/**
* An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the number of times we retry a to execute a
* given request, before reporting a failure. This includes unsuccessful return codes and timeouts.
*/
public class RetryInterceptor implements Interceptor {
// Delay between retying failed requests, in millis
private final long RETRY_TIME = 2000;
// Maximum number of times to retry the request before failing
private final int maxRetry;
// Internal counter for tracking the number of times we've tried this request
private int retryCounter = 0;
public RetryInterceptor(int maxRetry) {
this.maxRetry = maxRetry;
}
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
do {
try {
// If we are retrying a failed request that failed due to a bad response from the server, we must close it first
if (response != null) {
System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code())
+ "> from url -> " + chain.request().url() + ".");
response.close();
}
// System.out.println(chain.request().method() + " attempt <" + (retryCounter + 1) + "> to url -> " + chain.request().url());
response = chain.proceed(request);
} catch (IOException e) {
try {
// Include a small break in between requests.
Thread.sleep(RETRY_TIME);
} catch (InterruptedException e1) {
System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" + retryCounter + ">");
}
} finally {
retryCounter++;
}
} while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1));
/*
* if something has gone wrong, and we are unable to complete the request, we still need to initialize the return
* response so we don't get a null pointer exception.
*/
return response != null ? response : chain.proceed(request);
}
}

View File

@ -58,6 +58,19 @@
<optional>true</optional>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
<optional>true</optional>
</dependency>
<!-- Apache POI -->
<dependency>
<groupId>org.apache.poi</groupId>

View File

@ -1,684 +0,0 @@
package org.hl7.fhir.r4.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.r4.formats.IParser;
import org.hl7.fhir.r4.formats.IParser.OutputStyle;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.formats.XmlParser;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.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", "Java FHIR Client for FHIR");
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 > 1) {
System.out.println("Giving up: "+ioe.getMessage()+" (R4 / "+(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;
}
}

View File

@ -33,11 +33,11 @@ package org.hl7.fhir.r4.utils.client;
*/
import org.hl7.fhir.r4.model.OperationOutcome;
import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.r4.model.OperationOutcome;
/**
* FHIR client exception.
*
@ -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) {

View File

@ -34,26 +34,21 @@ package org.hl7.fhir.r4.utils.client;
*/
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.utilities.Utilities;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//Make resources address subclass of URI
/**
* Helper class to manage FHIR Resource URIs
*
@ -228,11 +223,8 @@ 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) {
public static ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
Matcher matcher = pattern.matcher(locationResponseHeader);
ResourceVersionedIdentifier parsedHeader = null;

View File

@ -1,109 +0,0 @@
package org.hl7.fhir.r4.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.r4.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;
}
}

View File

@ -0,0 +1,68 @@
package org.hl7.fhir.r4.utils.client.network;
import org.hl7.fhir.r4.formats.IParser;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.formats.XmlParser;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.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();
}
}

View File

@ -0,0 +1,194 @@
package org.hl7.fhir.r4.utils.client.network;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.client.EFhirClientException;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.io.IOException;
import java.net.URI;
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 ToolingClientLogger logger;
private int retryCount;
private long timeout = DEFAULT_TIMEOUT;
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 IOException {
Request.Builder request = new Request.Builder()
.method("OPTIONS", null)
.url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public int tester(int trytry) {
return 5;
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("PUT requests require a non-null payload");
RequestBody body = RequestBody.create(payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.put(body);
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 IOException {
return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public boolean issueDeleteRequest(URI resourceUri) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.delete();
return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest();
}
public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout);
}
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, new Headers.Builder().build(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
int timeout) throws IOException {
if (payload == null) throw new EFhirClientException("POST requests require a non-null payload");
RequestBody body = RequestBody.create(MediaType.parse(resourceFormat + ";charset=" + DEFAULT_CHARSET), payload);
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.post(body);
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
}
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
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();
}
public <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
int retryCount,
long timeout) throws IOException {
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();
}
}

View File

@ -0,0 +1,95 @@
package org.hl7.fhir.r4.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Generic Implementation of Client Headers.
*
* Stores a list of headers for HTTP calls to the TX server. Users can implement their own instance if they desire
* specific, custom behavior.
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
}
public ArrayList<Header> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", "
+ header.value + ">.");
}
headers.add(header);
return this;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
/**
* Removes the passed in header from the list of stored headers.
* @param header {@link Header} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", "
+ header.value + ">, from GenericClientHeaders that is not currently stored.");
}
return this;
}
/**
* Removes the passed in headers from the list of stored headers.
* @param headerList {@link List} of {@link Header} to remove.
* @throws FHIRException if any of the headers passed in does not exist within the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
return this;
}
@Override
public String toString() {
return this.headers.stream()
.map(header -> "\t" + header.name + ":" + header.value)
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -0,0 +1,328 @@
package org.hl7.fhir.r4.utils.client.network;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.formats.IParser;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.formats.XmlParser;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.ResourceUtilities;
import org.hl7.fhir.r4.utils.client.EFhirClientException;
import org.hl7.fhir.r4.utils.client.ResourceFormat;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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.r4.model.OperationOutcome.OperationOutcomeIssueComponent} within the
* provided {@link OperationOutcome} have an {@link org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity} of
* {@link org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity#ERROR} or
* {@link org.hl7.fhir.r4.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() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
T resource = unmarshalReference(response, resourceFormat);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
}
public Bundle executeAsBatch() throws IOException {
formatHeaders(httpRequest, resourceFormat, null);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
return unmarshalFeed(response, resourceFormat);
}
/**
* 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 { // TODO fix logs
// System.out.println("Call to log HTTP response with null ToolingClientLogger set... are you forgetting to " +
// "initialize your logger?");
// }
}
}

View File

@ -0,0 +1,71 @@
package org.hl7.fhir.r4.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.r4.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;
}
}

View File

@ -0,0 +1,62 @@
package org.hl7.fhir.r4.utils.client.network;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
/**
* An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the number of times we retry a to execute a
* given request, before reporting a failure. This includes unsuccessful return codes and timeouts.
*/
public class RetryInterceptor implements Interceptor {
// Delay between retying failed requests, in millis
private final long RETRY_TIME = 2000;
// Maximum number of times to retry the request before failing
private final int maxRetry;
// Internal counter for tracking the number of times we've tried this request
private int retryCounter = 0;
public RetryInterceptor(int maxRetry) {
this.maxRetry = maxRetry;
}
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
do {
try {
// If we are retrying a failed request that failed due to a bad response from the server, we must close it first
if (response != null) {
System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code())
+ "> from url -> " + chain.request().url() + ".");
response.close();
}
// System.out.println(chain.request().method() + " attempt <" + (retryCounter + 1) + "> to url -> " + chain.request().url());
response = chain.proceed(request);
} catch (IOException e) {
try {
// Include a small break in between requests.
Thread.sleep(RETRY_TIME);
} catch (InterruptedException e1) {
System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" + retryCounter + ">");
}
} finally {
retryCounter++;
}
} while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1));
/*
* if something has gone wrong, and we are unable to complete the request, we still need to initialize the return
* response so we don't get a null pointer exception.
*/
return response != null ? response : chain.proceed(request);
}
}

View File

@ -1,59 +1,57 @@
package org.hl7.fhir.r5.terminologies;
/*
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.
*/
/*
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.exceptions.FHIRException;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.ToolingClientLogger;
import java.util.Map;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.utilities.ToolingClientLogger;
public interface TerminologyClient {
public String getAddress();
public TerminologyCapabilities getTerminologyCapabilities() throws FHIRException;
public ValueSet expandValueset(ValueSet vs, Parameters p, Map<String, String> params) throws FHIRException;
public Parameters validateCS(Parameters pin) throws FHIRException;
public Parameters validateVS(Parameters pin) throws FHIRException;
public TerminologyClient setTimeout(int i) throws FHIRException;
public TerminologyClient setLogger(ToolingClientLogger txLog) throws FHIRException;
public int getRetryCount() throws FHIRException;
public TerminologyClient setRetryCount(int retryCount) throws FHIRException;
public CapabilityStatement getCapabilitiesStatementQuick() throws FHIRException;
public Parameters lookupCode(Map<String, String> params) throws FHIRException;
public Bundle validateBatch(Bundle batch);
public CanonicalResource read(String type, String id);
String getAddress();
TerminologyCapabilities getTerminologyCapabilities() throws FHIRException;
ValueSet expandValueset(ValueSet vs, Parameters p, Map<String, String> params) throws FHIRException;
Parameters validateCS(Parameters pin) throws FHIRException;
Parameters validateVS(Parameters pin) throws FHIRException;
TerminologyClient setTimeout(int i) throws FHIRException;
TerminologyClient setLogger(ToolingClientLogger txLog) throws FHIRException;
int getRetryCount() throws FHIRException;
TerminologyClient setRetryCount(int retryCount) throws FHIRException;
CapabilityStatement getCapabilitiesStatementQuick() throws FHIRException;
Parameters lookupCode(Map<String, String> params) throws FHIRException;
Bundle validateBatch(Bundle batch);
CanonicalResource read(String type, String id);
ClientHeaders getClientHeaders();
TerminologyClient setClientHeaders(ClientHeaders clientHeaders);
}

View File

@ -1,5 +1,7 @@
package org.hl7.fhir.r5.utils.client;
import okhttp3.Headers;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
/*
@ -35,6 +37,7 @@ import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
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.ClientHeaders;
import org.hl7.fhir.r5.utils.client.network.ResourceRequest;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.Utilities;
@ -43,8 +46,7 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
/**
* Very Simple RESTful client. This is purely for use in the standalone
@ -90,8 +92,11 @@ public class FHIRToolingClient {
private int maxResultSetSize = -1;//_count
private CapabilityStatement capabilities;
private Client client = new Client();
private ArrayList<Header> headers = new ArrayList<>();
private String username;
private String password;
//Pass enpoint for client - URI
//Pass endpoint for client - URI
public FHIRToolingClient(String baseServiceUrl) throws URISyntaxException {
preferredResourceFormat = ResourceFormat.RESOURCE_XML;
initialize(baseServiceUrl);
@ -104,6 +109,14 @@ public class FHIRToolingClient {
checkCapabilities();
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
private void checkCapabilities() {
try {
capabilities = getCapabilitiesStatementQuick();
@ -131,7 +144,10 @@ public class FHIRToolingClient {
TerminologyCapabilities capabilities = null;
try {
capabilities = (TerminologyCapabilities) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
getPreferredResourceFormat(), "TerminologyCapabilities", TIMEOUT_NORMAL).getReference();
getPreferredResourceFormat(),
generateHeaders(),
"TerminologyCapabilities",
TIMEOUT_NORMAL).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's terminology capabilities", e);
}
@ -142,7 +158,10 @@ public class FHIRToolingClient {
CapabilityStatement conformance = null;
try {
conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
getPreferredResourceFormat(), "CapabilitiesStatement", TIMEOUT_NORMAL).getReference();
getPreferredResourceFormat(),
generateHeaders(),
"CapabilitiesStatement",
TIMEOUT_NORMAL).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's conformance statement", e);
}
@ -153,7 +172,10 @@ public class FHIRToolingClient {
if (capabilities != null) return capabilities;
try {
capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
getPreferredResourceFormat(), "CapabilitiesStatement-Quick", TIMEOUT_NORMAL).getReference();
getPreferredResourceFormat(),
generateHeaders(),
"CapabilitiesStatement-Quick",
TIMEOUT_NORMAL).getReference();
} catch (Exception e) {
throw new FHIRException("Error fetching the server's capability statement: "+e.getMessage(), e);
}
@ -164,7 +186,10 @@ public class FHIRToolingClient {
ResourceRequest<T> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
getPreferredResourceFormat(), "Read " + resourceClass.getName() + "/" + id, TIMEOUT_NORMAL);
getPreferredResourceFormat(),
generateHeaders(),
"Read " + resourceClass.getName() + "/" + id,
TIMEOUT_NORMAL);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -178,7 +203,10 @@ public class FHIRToolingClient {
ResourceRequest<T> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
getPreferredResourceFormat(), "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version, TIMEOUT_NORMAL);
getPreferredResourceFormat(),
generateHeaders(),
"VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
TIMEOUT_NORMAL);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -192,7 +220,10 @@ public class FHIRToolingClient {
ResourceRequest<T> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
getPreferredResourceFormat(), "Read " + resourceClass.getName() + "?url=" + canonicalURL, TIMEOUT_NORMAL);
getPreferredResourceFormat(),
generateHeaders(),
"Read " + resourceClass.getName() + "?url=" + canonicalURL,
TIMEOUT_NORMAL);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -211,8 +242,11 @@ public class FHIRToolingClient {
org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(),
"Update " + resource.fhirType() + "/" + resource.getId(), TIMEOUT_OPERATION);
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(),
generateHeaders(),
"Update " + resource.fhirType() + "/" + resource.getId(),
TIMEOUT_OPERATION);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -236,7 +270,10 @@ public class FHIRToolingClient {
try {
result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(), "Update " + resource.fhirType() + "/" + id, TIMEOUT_OPERATION);
getPreferredResourceFormat(),
generateHeaders(),
"Update " + resource.fhirType() + "/" + id,
TIMEOUT_OPERATION);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -274,7 +311,7 @@ public class FHIRToolingClient {
"POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
} else {
client.getLogger().logRequest("GET", url.toString(), null, null);
result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
}
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
@ -307,7 +344,10 @@ public class FHIRToolingClient {
public <T extends Resource> OperationOutcome validate(Class<T> resourceClass, T resource, String id) {
ResourceRequest<T> 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);
result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(), generateHeaders(),
"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());
}
@ -362,7 +402,11 @@ public class FHIRToolingClient {
org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> 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);
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(),
generateHeaders(),
"ValueSet/$expand?url=" + source.getUrl(),
TIMEOUT_OPERATION_EXPAND);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -376,7 +420,11 @@ public class FHIRToolingClient {
public Parameters lookupCode(Map<String, String> params) {
org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), getPreferredResourceFormat(), "CodeSystem/$lookup", TIMEOUT_NORMAL);
result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
getPreferredResourceFormat(),
generateHeaders(),
"CodeSystem/$lookup",
TIMEOUT_NORMAL);
} catch (IOException e) {
e.printStackTrace();
}
@ -394,8 +442,13 @@ public class FHIRToolingClient {
}
org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> 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);
ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(),
generateHeaders(),
"ValueSet/$expand?url=" + source.getUrl(),
TIMEOUT_OPERATION_EXPAND);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -415,7 +468,11 @@ public class FHIRToolingClient {
ResourceRequest<Resource> result = null;
try {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), null, "Closure?name=" + name, TIMEOUT_NORMAL);
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(),
generateHeaders(),
"Closure?name=" + name,
TIMEOUT_NORMAL);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -432,7 +489,11 @@ public class FHIRToolingClient {
org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), null, "UpdateClosure?name=" + name, TIMEOUT_OPERATION);
ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
getPreferredResourceFormat(),
generateHeaders(),
"UpdateClosure?name=" + name,
TIMEOUT_OPERATION);
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
}
@ -442,6 +503,22 @@ public class FHIRToolingClient {
return result == null ? null : (ConceptMap) result.getPayload();
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getTimeout() {
return client.getTimeout();
}
@ -466,6 +543,31 @@ public class FHIRToolingClient {
client.setRetryCount(retryCount);
}
public void setClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
}
private Headers generateHeaders() {
Headers.Builder builder = new Headers.Builder();
// Add basic auth header if it exists
if (basicAuthHeaderExists()) {
builder.add(getAuthorizationHeader().toString());
}
// Add any other headers
if(this.headers != null) {
this.headers.forEach(header -> builder.add(header.toString()));
}
return builder.build();
}
public boolean basicAuthHeaderExists() {
return (username != null) && (password != null);
}
public Header getAuthorizationHeader() {
String usernamePassword = username + ":" + password;
String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
return new Header("Authorization", "Basic " + base64usernamePassword);
}
}

View File

@ -10,14 +10,7 @@ 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;
@ -25,28 +18,10 @@ 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;
}
@ -79,25 +54,29 @@ public class Client {
.method("OPTIONS", null)
.url(optionsUri.toURL());
return executeFhirRequest(request, resourceFormat, null, message, retryCount, timeout);
return executeFhirRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
}
public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri,
String resourceFormat,
Headers headers,
String message,
long timeout) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeFhirRequest(request, resourceFormat, null, message, retryCount, timeout);
return executeFhirRequest(request, resourceFormat, headers, message, retryCount, timeout);
}
public int tester(int trytry) {
return 5;
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
byte[] payload,
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePutRequest(resourceUri, payload, resourceFormat, null, message, timeout);
return issuePutRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri,
@ -120,7 +99,7 @@ public class Client {
String resourceFormat,
String message,
long timeout) throws IOException {
return issuePostRequest(resourceUri, payload, resourceFormat, null, message, timeout);
return issuePostRequest(resourceUri, payload, resourceFormat, new Headers.Builder().build(), message, timeout);
}
public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri,
@ -142,14 +121,14 @@ public class Client {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL())
.delete();
return executeFhirRequest(request, null, null, null, retryCount, timeout).isSuccessfulRequest();
return executeFhirRequest(request, null, new Headers.Builder().build(), null, retryCount, timeout).isSuccessfulRequest();
}
public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) throws IOException {
Request.Builder request = new Request.Builder()
.url(resourceUri.toURL());
return executeBundleRequest(request, resourceFormat, null, null, retryCount, timeout);
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout);
}
public Bundle issuePostFeedRequest(URI resourceUri,
@ -164,7 +143,7 @@ public class Client {
.url(resourceUri.toURL())
.post(body);
return executeBundleRequest(request, resourceFormat, null, null, retryCount, timeout);
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), null, retryCount, timeout);
}
public Bundle postBatchRequest(URI resourceUri,
@ -178,10 +157,10 @@ public class Client {
.url(resourceUri.toURL())
.post(body);
return executeBundleRequest(request, resourceFormat, null, message, retryCount, timeout);
return executeBundleRequest(request, resourceFormat, new Headers.Builder().build(), message, retryCount, timeout);
}
protected <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
public <T extends Resource> Bundle executeBundleRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
@ -197,7 +176,7 @@ public class Client {
.executeAsBatch();
}
protected <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
public <T extends Resource> ResourceRequest<T> executeFhirRequest(Request.Builder request,
String resourceFormat,
Headers headers,
String message,
@ -212,23 +191,4 @@ public class Client {
.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);
}
}
}

View File

@ -0,0 +1,95 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Generic Implementation of Client Headers.
*
* Stores a list of headers for HTTP calls to the TX server. Users can implement their own instance if they desire
* specific, custom behavior.
*/
public class ClientHeaders {
private final ArrayList<Header> headers;
public ClientHeaders() {
this.headers = new ArrayList<>();
}
public ClientHeaders(ArrayList<Header> headers) {
this.headers = headers;
}
public ArrayList<Header> headers() {
return headers;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param header {@link Header} to add to the list.
* @throws FHIRException if the header being added is a duplicate.
*/
public ClientHeaders addHeader(Header header) throws FHIRException {
if (headers.contains(header)) {
throw new FHIRException("Attempting to add duplicate header, <" + header.name + ", "
+ header.value + ">.");
}
headers.add(header);
return this;
}
/**
* Add a header to the list of stored headers for network operations.
*
* @param headerList {@link List} of {@link Header} to add.
* @throws FHIRException if any of the headers being added is a duplicate.
*/
public ClientHeaders addHeaders(List<Header> headerList) throws FHIRException {
headerList.forEach(this::addHeader);
return this;
}
/**
* Removes the passed in header from the list of stored headers.
* @param header {@link Header} to remove from the list.
* @throws FHIRException if the header passed in does not exist within the stored list.
*/
public ClientHeaders removeHeader(Header header) throws FHIRException {
if (!headers.remove(header)) {
throw new FHIRException("Attempting to remove header, <" + header.name + ", "
+ header.value + ">, from GenericClientHeaders that is not currently stored.");
}
return this;
}
/**
* Removes the passed in headers from the list of stored headers.
* @param headerList {@link List} of {@link Header} to remove.
* @throws FHIRException if any of the headers passed in does not exist within the stored list.
*/
public ClientHeaders removeHeaders(List<Header> headerList) throws FHIRException {
headerList.forEach(this::removeHeader);
return this;
}
/**
* Clears all stored {@link Header}.
*/
public ClientHeaders clearHeaders() {
headers.clear();
return this;
}
@Override
public String toString() {
return this.headers.stream()
.map(header -> "\t" + header.name + ":" + header.value)
.collect(Collectors.joining(",\n", "{\n", "\n}"));
}
}

View File

@ -0,0 +1,241 @@
package org.hl7.fhir.r5.utils.client;
import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.utils.client.network.Client;
import org.hl7.fhir.r5.utils.client.network.ResourceRequest;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
class FHIRToolingClientTest {
String TX_ADDR = "http://tx.fhir.org";
Header h1 = new Header("header1", "value1");
Header h2 = new Header("header2", "value2");
Header h3 = new Header("header3", "value3");
private Client mockClient;
private FHIRToolingClient toolingClient;
@BeforeEach
void setUp() throws IOException, URISyntaxException {
mockClient = Mockito.mock(Client.class);
ResourceRequest<Resource> resourceResourceRequest = new ResourceRequest<>(generateBundle(), 200, "");
//GET
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong()))
.thenReturn(resourceResourceRequest);
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("TerminologyCapabilities"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new TerminologyCapabilities(), 200, "location"));
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
Mockito.when(mockClient.issueGetResourceRequest(Mockito.any(URI.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.eq("CapabilitiesStatement-Quick"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new CapabilityStatement(), 200, "location"));
//PUT
Mockito.when(mockClient.issuePutRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong()))
.thenReturn(resourceResourceRequest);
//POST
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyLong()))
.thenReturn(resourceResourceRequest);
Mockito.when(mockClient.issuePostRequest(Mockito.any(URI.class), Mockito.any(byte[].class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.contains("validate"), Mockito.anyLong()))
.thenReturn(new ResourceRequest<>(new OperationOutcome(), 200, "location"));
//BUNDLE REQ
Mockito.when(mockClient.executeBundleRequest(Mockito.any(Request.Builder.class), Mockito.anyString(),
Mockito.any(Headers.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()))
.thenReturn(generateBundle());
toolingClient = new FHIRToolingClient(TX_ADDR);
toolingClient.setClient(mockClient);
}
private ArrayList<Header> getHeaders() {
return new ArrayList<>(Arrays.asList(h1, h2, h3));
}
private Bundle generateBundle() {
Patient patient = generatePatient();
Observation observation = generateObservation();
// The observation refers to the patient using the ID, which is already
// set to a temporary UUID
observation.setSubject(new Reference(patient.getIdElement().getValue()));
// Create a bundle that will be used as a transaction
Bundle bundle = new Bundle();
// Add the patient as an entry.
bundle.addEntry()
.setFullUrl(patient.getIdElement().getValue())
.setResource(patient)
.getRequest()
.setUrl("Patient")
.setIfNoneExist("identifier=http://acme.org/mrns|12345")
.setMethod(Bundle.HTTPVerb.POST);
return bundle;
}
@NotNull
private Patient generatePatient() {
// Create a patient object
Patient patient = new Patient();
patient.addIdentifier()
.setSystem("http://acme.org/mrns")
.setValue("12345");
patient.addName()
.setFamily("Jameson")
.addGiven("J")
.addGiven("Jonah");
patient.setGender(Enumerations.AdministrativeGender.MALE);
// Give the patient a temporary UUID so that other resources in
// the transaction can refer to it
patient.setId(IdType.newRandomUuid());
return patient;
}
@NotNull
private Observation generateObservation() {
// Create an observation object
Observation observation = new Observation();
observation
.getCode()
.addCoding()
.setSystem("http://loinc.org")
.setCode("789-8")
.setDisplay("Erythrocytes [#/volume] in Blood by Automated count");
observation.setValue(
new Quantity()
.setValue(4.12)
.setUnit("10 trillion/L")
.setSystem("http://unitsofmeasure.org")
.setCode("10*12/L"));
return observation;
}
private void checkHeaders(Headers argumentCaptorValue) {
getHeaders().forEach(header -> {
System.out.println("Checking header <" + header.component1().utf8() + ", " + header.component2().utf8() + ">");
Assertions.assertEquals(argumentCaptorValue.get(header.component1().utf8()), header.component2().utf8());
});
}
@Test
void getTerminologyCapabilities() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getTerminologyCapabilities();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatement() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatement();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCapabilitiesStatementQuick() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCapabilitiesStatementQuick();
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void read() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.read(Patient.class, "id");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void vread() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.vread(Patient.class, "id", "version");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void getCanonical() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.getCanonical(Patient.class, "canonicalURL");
Mockito.verify(mockClient).issueGetResourceRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.anyString(),
headersArgumentCaptor.capture(), ArgumentMatchers.anyString(), ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void update() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.update(generatePatient());
Mockito.verify(mockClient).issuePutRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
@Test
void validate() throws IOException {
ArgumentCaptor<Headers> headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class);
toolingClient.setClientHeaders(getHeaders());
toolingClient.validate(Patient.class, generatePatient(), "id");
Mockito.verify(mockClient).issuePostRequest(ArgumentMatchers.any(URI.class), ArgumentMatchers.any(byte[].class),
ArgumentMatchers.anyString(), headersArgumentCaptor.capture(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyLong());
Headers argumentCaptorValue = headersArgumentCaptor.getValue();
checkHeaders(argumentCaptorValue);
}
}

View File

@ -0,0 +1,105 @@
package org.hl7.fhir.r5.utils.client.network;
import okhttp3.internal.http2.Header;
import org.hl7.fhir.exceptions.FHIRException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
class ClientHeadersTest {
ClientHeaders clientHeaders;
Header h1 = new Header("header1", "value1");
Header h2 = new Header("header2", "value2");
Header h3 = new Header("header3", "value3");
@BeforeEach
void setUp() {
clientHeaders = new ClientHeaders();
}
@Test
@DisplayName("Happy path add headers individually.")
void addHeader() {
clientHeaders.addHeader(h1);
Assertions.assertEquals(1, clientHeaders.headers().size());
clientHeaders.addHeader(h2);
Assertions.assertEquals(2, clientHeaders.headers().size());
}
@Test
@DisplayName("Test duplicate header added individually throws FHIRException.")
void addHeaderDuplicateAdd() {
clientHeaders.addHeader(h1);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeader(h1));
}
@Test
@DisplayName("Happy path add headers as list.")
void addHeaders() {
List<Header> headersList = Arrays.asList(h1, h2, h3);
clientHeaders.addHeaders(headersList);
Assertions.assertEquals(3, clientHeaders.headers().size());
Assertions.assertEquals(headersList, clientHeaders.headers());
}
@Test
@DisplayName("Happy path add headers as list.")
void addHeadersDuplicateAdd() {
List<Header> headersList = Arrays.asList(h1, h2, h1);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.addHeaders(headersList));
}
@Test
@DisplayName("Happy path remove existing header.")
void removeHeader() {
clientHeaders.addHeader(h1);
clientHeaders.addHeader(h2);
clientHeaders.addHeader(h3);
clientHeaders.removeHeader(h2);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.removeHeader(new Header("header3", "value3"));
Assertions.assertEquals(1, clientHeaders.headers().size());
}
@Test
@DisplayName("Remove header not contained in list.")
void removeHeaderUnknown() {
clientHeaders.addHeader(h1);
clientHeaders.addHeader(h2);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeader(h3));
}
@Test
@DisplayName("Happy path remove list of existing headers.")
void removeHeaders() {
List<Header> headersToAddList = Arrays.asList(h1, h2, h3);
List<Header> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
clientHeaders.removeHeaders(headersToRemoveList);
Assertions.assertEquals(1, clientHeaders.headers().size());
}
@Test
@DisplayName("Remove list containing unknown header.")
void removeHeadersUnknown() {
List<Header> headersToAddList = Arrays.asList(h1, h3);
List<Header> headersToRemoveList = Arrays.asList(h2, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertThrows(FHIRException.class, () -> clientHeaders.removeHeaders(headersToRemoveList));
}
@Test
void clearHeaders() {
List<Header> headersToAddList = Arrays.asList(h1, h3);
clientHeaders.addHeaders(headersToAddList);
Assertions.assertEquals(2, clientHeaders.headers().size());
clientHeaders.clearHeaders();
Assertions.assertEquals(0, clientHeaders.headers().size());
}
}

View File

@ -23,14 +23,14 @@ class ClientTest {
private HttpUrl serverUrl;
private Client client;
private Address address = new Address()
private final Address address = new Address()
.setCity("Toronto")
.setState("Ontario")
.setCountry("Canada");
private HumanName humanName = new HumanName()
private final HumanName humanName = new HumanName()
.addGiven("Mark")
.setFamily("Iantorno");
private Patient patient = new Patient()
private final Patient patient = new Patient()
.addName(humanName)
.addAddress(address)
.setGender(Enumerations.AdministrativeGender.MALE);
@ -58,7 +58,7 @@ class ClientTest {
.setBody(new String(generateResourceBytes(patient)))
);
ResourceRequest<Resource> resourceRequest = client.issueGetResourceRequest(new URI(serverUrl.toString()),
"json", null, TIMEOUT);
"json", null, null, TIMEOUT);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");
@ -80,7 +80,7 @@ class ClientTest {
client.setRetryCount(failedAttempts + 1);
ResourceRequest<Resource> resourceRequest = client.issueGetResourceRequest(new URI(serverUrl.toString()),
"json", null, TIMEOUT);
"json", null, null, TIMEOUT);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");
@ -102,7 +102,7 @@ class ClientTest {
client.setRetryCount(failedAttempts + 1);
ResourceRequest<Resource> resourceRequest = client.issueGetResourceRequest(new URI(serverUrl.toString()),
"json", null, TIMEOUT);
"json", null, null, TIMEOUT);
Assertions.assertTrue(resourceRequest.isSuccessfulRequest());
Assertions.assertTrue(patient.equalsDeep(resourceRequest.getPayload()),
"GET request returned resource does not match expected.");