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