001package org.hl7.fhir.r4.utils.client; 002 003 004/* 005 Copyright (c) 2011+, HL7, Inc. 006 All rights reserved. 007 008 Redistribution and use in source and binary forms, with or without modification, 009 are permitted provided that the following conditions are met: 010 011 * Redistributions of source code must retain the above copyright notice, this 012 list of conditions and the following disclaimer. 013 * Redistributions in binary form must reproduce the above copyright notice, 014 this list of conditions and the following disclaimer in the documentation 015 and/or other materials provided with the distribution. 016 * Neither the name of HL7 nor the names of its contributors may be used to 017 endorse or promote products derived from this software without specific 018 prior written permission. 019 020 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 021 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 022 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 023 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 024 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 025 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 026 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 027 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 028 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 029 POSSIBILITY OF SUCH DAMAGE. 030 031 */ 032 033 034import java.io.ByteArrayOutputStream; 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.OutputStreamWriter; 038import java.io.UnsupportedEncodingException; 039import java.net.HttpURLConnection; 040import java.net.MalformedURLException; 041import java.net.URI; 042import java.net.URLConnection; 043import java.nio.charset.StandardCharsets; 044import java.text.ParseException; 045import java.text.SimpleDateFormat; 046import java.util.ArrayList; 047import java.util.Calendar; 048import java.util.Date; 049import java.util.List; 050import java.util.Map; 051 052import org.apache.commons.codec.binary.Base64; 053import org.apache.commons.io.IOUtils; 054import org.apache.commons.lang3.CharSet; 055import org.apache.commons.lang3.StringUtils; 056import org.apache.http.Header; 057import org.apache.http.HttpEntity; 058import org.apache.http.HttpEntityEnclosingRequest; 059import org.apache.http.HttpHost; 060import org.apache.http.HttpRequest; 061import org.apache.http.HttpResponse; 062import org.apache.http.client.HttpClient; 063import org.apache.http.client.methods.HttpDelete; 064import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 065import org.apache.http.client.methods.HttpGet; 066import org.apache.http.client.methods.HttpOptions; 067import org.apache.http.client.methods.HttpPost; 068import org.apache.http.client.methods.HttpPut; 069import org.apache.http.client.methods.HttpUriRequest; 070import org.apache.http.conn.params.ConnRoutePNames; 071import org.apache.http.entity.ByteArrayEntity; 072import org.apache.http.impl.client.DefaultHttpClient; 073import org.apache.http.params.HttpConnectionParams; 074import org.apache.http.params.HttpParams; 075import org.hl7.fhir.r4.formats.IParser; 076import org.hl7.fhir.r4.formats.IParser.OutputStyle; 077import org.hl7.fhir.r4.formats.JsonParser; 078import org.hl7.fhir.r4.formats.XmlParser; 079import org.hl7.fhir.r4.model.Bundle; 080import org.hl7.fhir.r4.model.OperationOutcome; 081import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; 082import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; 083import org.hl7.fhir.r4.model.Resource; 084import org.hl7.fhir.r4.model.ResourceType; 085import org.hl7.fhir.r4.utils.ResourceUtilities; 086 087/** 088 * Helper class handling lower level HTTP transport concerns. 089 * TODO Document methods. 090 * @author Claude Nanjo 091 */ 092public class ClientUtils { 093 094 public static final String DEFAULT_CHARSET = "UTF-8"; 095 public static final String HEADER_LOCATION = "location"; 096 097 private HttpHost proxy; 098 private int timeout = 5000; 099 private String username; 100 private String password; 101 private ToolingClientLogger logger; 102 103 public HttpHost getProxy() { 104 return proxy; 105 } 106 107 public void setProxy(HttpHost proxy) { 108 this.proxy = proxy; 109 } 110 111 public int getTimeout() { 112 return timeout; 113 } 114 115 public void setTimeout(int timeout) { 116 this.timeout = timeout; 117 } 118 119 public String getUsername() { 120 return username; 121 } 122 123 public void setUsername(String username) { 124 this.username = username; 125 } 126 127 public String getPassword() { 128 return password; 129 } 130 131 public void setPassword(String password) { 132 this.password = password; 133 } 134 135 public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat) { 136 HttpOptions options = new HttpOptions(optionsUri); 137 return issueResourceRequest(resourceFormat, options); 138 } 139 140 public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat) { 141 HttpGet httpget = new HttpGet(resourceUri); 142 return issueResourceRequest(resourceFormat, httpget); 143 } 144 145 public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers) { 146 HttpPut httpPut = new HttpPut(resourceUri); 147 return issueResourceRequest(resourceFormat, httpPut, payload, headers); 148 } 149 150 public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat) { 151 HttpPut httpPut = new HttpPut(resourceUri); 152 return issueResourceRequest(resourceFormat, httpPut, payload, null); 153 } 154 155 public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers) { 156 HttpPost httpPost = new HttpPost(resourceUri); 157 return issueResourceRequest(resourceFormat, httpPost, payload, headers); 158 } 159 160 161 public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat) { 162 return issuePostRequest(resourceUri, payload, resourceFormat, null); 163 } 164 165 public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) { 166 HttpGet httpget = new HttpGet(resourceUri); 167 configureFhirRequest(httpget, resourceFormat); 168 HttpResponse response = sendRequest(httpget); 169 return unmarshalReference(response, resourceFormat); 170 } 171 172 private void setAuth(HttpRequest httpget) { 173 if (password != null) { 174 try { 175 byte[] b = Base64.encodeBase64((username+":"+password).getBytes("ASCII")); 176 String b64 = new String(b, StandardCharsets.US_ASCII); 177 httpget.setHeader("Authorization", "Basic " + b64); 178 } catch (UnsupportedEncodingException e) { 179 } 180 } 181 } 182 183 public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat) { 184 HttpPost httpPost = new HttpPost(resourceUri); 185 configureFhirRequest(httpPost, resourceFormat); 186 HttpResponse response = sendPayload(httpPost, payload, proxy); 187 return unmarshalFeed(response, resourceFormat); 188 } 189 190 public boolean issueDeleteRequest(URI resourceUri) { 191 HttpDelete deleteRequest = new HttpDelete(resourceUri); 192 HttpResponse response = sendRequest(deleteRequest); 193 int responseStatusCode = response.getStatusLine().getStatusCode(); 194 boolean deletionSuccessful = false; 195 if(responseStatusCode == 204) { 196 deletionSuccessful = true; 197 } 198 return deletionSuccessful; 199 } 200 201 /*********************************************************** 202 * Request/Response Helper methods 203 ***********************************************************/ 204 205 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request) { 206 return issueResourceRequest(resourceFormat, request, null); 207 } 208 209 /** 210 * @param resourceFormat 211 * @param options 212 * @return 213 */ 214 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload) { 215 return issueResourceRequest(resourceFormat, request, payload, null); 216 } 217 218 /** 219 * @param resourceFormat 220 * @param options 221 * @return 222 */ 223 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List<Header> headers) { 224 configureFhirRequest(request, resourceFormat, headers); 225 HttpResponse response = null; 226 if(request instanceof HttpEntityEnclosingRequest && payload != null) { 227 response = sendPayload((HttpEntityEnclosingRequestBase)request, payload, proxy); 228 } else if (request instanceof HttpEntityEnclosingRequest && payload == null){ 229 throw new EFhirClientException("PUT and POST requests require a non-null payload"); 230 } else { 231 response = sendRequest(request); 232 } 233 T resource = unmarshalReference(response, resourceFormat); 234 return new ResourceRequest<T>(resource, response.getStatusLine().getStatusCode(), getLocationHeader(response)); 235 } 236 237 238 /** 239 * Method adds required request headers. 240 * TODO handle JSON request as well. 241 * 242 * @param request 243 */ 244 protected void configureFhirRequest(HttpRequest request, String format) { 245 configureFhirRequest(request, format, null); 246 } 247 248 /** 249 * Method adds required request headers. 250 * TODO handle JSON request as well. 251 * 252 * @param request 253 */ 254 protected void configureFhirRequest(HttpRequest request, String format, List<Header> headers) { 255 request.addHeader("User-Agent", "Java FHIR Client for FHIR"); 256 257 if (format != null) { 258 request.addHeader("Accept",format); 259 request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); 260 } 261 request.addHeader("Accept-Charset", DEFAULT_CHARSET); 262 if(headers != null) { 263 for(Header header : headers) { 264 request.addHeader(header); 265 } 266 } 267 setAuth(request); 268 } 269 270 /** 271 * Method posts request payload 272 * 273 * @param request 274 * @param payload 275 * @return 276 */ 277 protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy) { 278 HttpResponse response = null; 279 try { 280 HttpClient httpclient = new DefaultHttpClient(); 281 if(proxy != null) { 282 httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); 283 } 284 request.setEntity(new ByteArrayEntity(payload)); 285 log(request); 286 response = httpclient.execute(request); 287 } catch(IOException ioe) { 288 throw new EFhirClientException("Error sending HTTP Post/Put Payload", ioe); 289 } 290 return response; 291 } 292 293 /** 294 * 295 * @param request 296 * @param payload 297 * @return 298 */ 299 protected HttpResponse sendRequest(HttpUriRequest request) { 300 HttpResponse response = null; 301 try { 302 HttpClient httpclient = new DefaultHttpClient(); 303 log(request); 304 HttpParams params = httpclient.getParams(); 305 HttpConnectionParams.setConnectionTimeout(params, timeout); 306 HttpConnectionParams.setSoTimeout(params, timeout); 307 if(proxy != null) { 308 httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); 309 } 310 response = httpclient.execute(request); 311 } catch(IOException ioe) { 312 throw new EFhirClientException("Error sending Http Request: "+ioe.getMessage(), ioe); 313 } 314 return response; 315 } 316 317 318 /** 319 * Unmarshals a resource from the response stream. 320 * 321 * @param response 322 * @return 323 */ 324 @SuppressWarnings("unchecked") 325 protected <T extends Resource> T unmarshalReference(HttpResponse response, String format) { 326 T resource = null; 327 OperationOutcome error = null; 328 byte[] cnt = log(response); 329 if (cnt != null) { 330 try { 331 resource = (T)getParser(format).parse(cnt); 332 if (resource instanceof OperationOutcome && hasError((OperationOutcome)resource)) { 333 error = (OperationOutcome) resource; 334 } 335 } catch(IOException ioe) { 336 throw new EFhirClientException("Error reading Http Response: "+ioe.getMessage(), ioe); 337 } catch(Exception e) { 338 throw new EFhirClientException("Error parsing response message: "+e.getMessage(), e); 339 } 340 } 341 if(error != null) { 342 throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error); 343 } 344 return resource; 345 } 346 347 /** 348 * Unmarshals Bundle from response stream. 349 * 350 * @param response 351 * @return 352 */ 353 protected Bundle unmarshalFeed(HttpResponse response, String format) { 354 Bundle feed = null; 355 byte[] cnt = log(response); 356 String contentType = response.getHeaders("Content-Type")[0].getValue(); 357 OperationOutcome error = null; 358 try { 359 if (cnt != null) { 360 if(contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) { 361 Resource rf = getParser(format).parse(cnt); 362 if (rf instanceof Bundle) 363 feed = (Bundle) rf; 364 else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { 365 error = (OperationOutcome) rf; 366 } else { 367 throw new EFhirClientException("Error reading server response: a resource was returned instead"); 368 } 369 } 370 } 371 } catch(IOException ioe) { 372 throw new EFhirClientException("Error reading Http Response", ioe); 373 } catch(Exception e) { 374 throw new EFhirClientException("Error parsing response message", e); 375 } 376 if(error != null) { 377 throw new EFhirClientException("Error from server: "+ResourceUtilities.getErrorDescription(error), error); 378 } 379 return feed; 380 } 381 382 private boolean hasError(OperationOutcome oo) { 383 for (OperationOutcomeIssueComponent t : oo.getIssue()) 384 if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL) 385 return true; 386 return false; 387 } 388 389 protected String getLocationHeader(HttpResponse response) { 390 String location = null; 391 if(response.getHeaders("location").length > 0) {//TODO Distinguish between both cases if necessary 392 location = response.getHeaders("location")[0].getValue(); 393 } else if(response.getHeaders("content-location").length > 0) { 394 location = response.getHeaders("content-location")[0].getValue(); 395 } 396 return location; 397 } 398 399 400 /***************************************************************** 401 * Client connection methods 402 * ***************************************************************/ 403 404 public HttpURLConnection buildConnection(URI baseServiceUri, String tail) { 405 try { 406 HttpURLConnection client = (HttpURLConnection) baseServiceUri.resolve(tail).toURL().openConnection(); 407 return client; 408 } catch(MalformedURLException mue) { 409 throw new EFhirClientException("Invalid Service URL", mue); 410 } catch(IOException ioe) { 411 throw new EFhirClientException("Unable to establish connection to server: " + baseServiceUri.toString() + tail, ioe); 412 } 413 } 414 415 public HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) { 416 return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id)); 417 } 418 419 /****************************************************************** 420 * Other general helper methods 421 * ****************************************************************/ 422 423 424 public <T extends Resource> byte[] getResourceAsByteArray(T resource, boolean pretty, boolean isJson) { 425 ByteArrayOutputStream baos = null; 426 byte[] byteArray = null; 427 try { 428 baos = new ByteArrayOutputStream(); 429 IParser parser = null; 430 if(isJson) { 431 parser = new JsonParser(); 432 } else { 433 parser = new XmlParser(); 434 } 435 parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); 436 parser.compose(baos, resource); 437 baos.close(); 438 byteArray = baos.toByteArray(); 439 baos.close(); 440 } catch (Exception e) { 441 try{ 442 baos.close(); 443 }catch(Exception ex) { 444 throw new EFhirClientException("Error closing output stream", ex); 445 } 446 throw new EFhirClientException("Error converting output stream to byte array", e); 447 } 448 return byteArray; 449 } 450 451 public byte[] getFeedAsByteArray(Bundle feed, boolean pretty, boolean isJson) { 452 ByteArrayOutputStream baos = null; 453 byte[] byteArray = null; 454 try { 455 baos = new ByteArrayOutputStream(); 456 IParser parser = null; 457 if(isJson) { 458 parser = new JsonParser(); 459 } else { 460 parser = new XmlParser(); 461 } 462 parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); 463 parser.compose(baos, feed); 464 baos.close(); 465 byteArray = baos.toByteArray(); 466 baos.close(); 467 } catch (Exception e) { 468 try{ 469 baos.close(); 470 }catch(Exception ex) { 471 throw new EFhirClientException("Error closing output stream", ex); 472 } 473 throw new EFhirClientException("Error converting output stream to byte array", e); 474 } 475 return byteArray; 476 } 477 478 public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) { 479 String dateTime = null; 480 try { 481 dateTime = serverConnection.getHeaderField("Last-Modified"); 482 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); 483 Date lastModifiedTimestamp = format.parse(dateTime); 484 Calendar calendar=Calendar.getInstance(); 485 calendar.setTime(lastModifiedTimestamp); 486 return calendar; 487 } catch(ParseException pe) { 488 throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe); 489 } 490 } 491 492 protected IParser getParser(String format) { 493 if(StringUtils.isBlank(format)) { 494 format = ResourceFormat.RESOURCE_XML.getHeader(); 495 } 496 if(format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) { 497 return new JsonParser(); 498 } else if(format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) { 499 return new XmlParser(); 500 } else { 501 throw new EFhirClientException("Invalid format: " + format); 502 } 503 } 504 505 public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName, Resource resource, String resourceFormat) throws IOException { 506 HttpPost httppost = new HttpPost(resourceUri); 507 String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; 508 httppost.addHeader("Content-Type", "multipart/form-data; boundary="+boundary); 509 httppost.addHeader("Accept", resourceFormat); 510 configureFhirRequest(httppost, null); 511 HttpResponse response = sendPayload(httppost, encodeFormSubmission(parameters, resourceName, resource, boundary)); 512 return unmarshalFeed(response, resourceFormat); 513 } 514 515 private byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource, String boundary) throws IOException { 516 ByteArrayOutputStream b = new ByteArrayOutputStream(); 517 OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8"); 518 for (String name : parameters.keySet()) { 519 w.write("--"); 520 w.write(boundary); 521 w.write("\r\nContent-Disposition: form-data; name=\""+name+"\"\r\n\r\n"); 522 w.write(parameters.get(name)+"\r\n"); 523 } 524 w.write("--"); 525 w.write(boundary); 526 w.write("\r\nContent-Disposition: form-data; name=\""+resourceName+"\"\r\n\r\n"); 527 w.close(); 528 JsonParser json = new JsonParser(); 529 json.setOutputStyle(OutputStyle.NORMAL); 530 json.compose(b, resource); 531 b.close(); 532 w = new OutputStreamWriter(b, "UTF-8"); 533 w.write("\r\n--"); 534 w.write(boundary); 535 w.write("--"); 536 w.close(); 537 return b.toByteArray(); 538 } 539 540 /** 541 * Method posts request payload 542 * 543 * @param request 544 * @param payload 545 * @return 546 */ 547 protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) { 548 HttpResponse response = null; 549 try { 550 log(request); 551 HttpClient httpclient = new DefaultHttpClient(); 552 request.setEntity(new ByteArrayEntity(payload)); 553 response = httpclient.execute(request); 554 log(response); 555 } catch(IOException ioe) { 556 throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe); 557 } 558 return response; 559 } 560 561 private void log(HttpUriRequest request) { 562 if (logger != null) { 563 List<String> headers = new ArrayList<>(); 564 for (Header h : request.getAllHeaders()) { 565 headers.add(h.toString()); 566 } 567 logger.logRequest(request.getMethod(), request.getURI().toString(), headers, null); 568 } 569 } 570 private void log(HttpEntityEnclosingRequestBase request) { 571 if (logger != null) { 572 List<String> headers = new ArrayList<>(); 573 for (Header h : request.getAllHeaders()) { 574 headers.add(h.toString()); 575 } 576 byte[] cnt = null; 577 InputStream s; 578 try { 579 s = request.getEntity().getContent(); 580 cnt = IOUtils.toByteArray(s); 581 s.close(); 582 } catch (Exception e) { 583 } 584 logger.logRequest(request.getMethod(), request.getURI().toString(), headers, cnt); 585 } 586 } 587 588 private byte[] log(HttpResponse response) { 589 byte[] cnt = null; 590 try { 591 InputStream s = response.getEntity().getContent(); 592 cnt = IOUtils.toByteArray(s); 593 s.close(); 594 } catch (Exception e) { 595 } 596 if (logger != null) { 597 List<String> headers = new ArrayList<>(); 598 for (Header h : response.getAllHeaders()) { 599 headers.add(h.toString()); 600 } 601 logger.logResponse(response.getStatusLine().toString(), headers, cnt); 602 } 603 return cnt; 604 } 605 606 public ToolingClientLogger getLogger() { 607 return logger; 608 } 609 610 public void setLogger(ToolingClientLogger logger) { 611 this.logger = logger; 612 } 613 614 615 /** 616 * Used for debugging 617 * 618 * @param instream 619 * @return 620 */ 621 protected String writeInputStreamAsString(InputStream instream) { 622 String value = null; 623 try { 624 value = IOUtils.toString(instream, "UTF-8"); 625 System.out.println(value); 626 627 } catch(IOException ioe) { 628 //Do nothing 629 } 630 return value; 631 } 632 633 634}