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