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.Calendar; 047import java.util.Date; 048import java.util.List; 049import java.util.Map; 050 051import org.apache.commons.codec.binary.Base64; 052import org.apache.commons.io.IOUtils; 053import org.apache.commons.lang3.CharSet; 054import org.apache.commons.lang3.StringUtils; 055import org.apache.http.Header; 056import org.apache.http.HttpEntity; 057import org.apache.http.HttpEntityEnclosingRequest; 058import org.apache.http.HttpHost; 059import org.apache.http.HttpRequest; 060import org.apache.http.HttpResponse; 061import org.apache.http.client.HttpClient; 062import org.apache.http.client.methods.HttpDelete; 063import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 064import org.apache.http.client.methods.HttpGet; 065import org.apache.http.client.methods.HttpOptions; 066import org.apache.http.client.methods.HttpPost; 067import org.apache.http.client.methods.HttpPut; 068import org.apache.http.client.methods.HttpUriRequest; 069import org.apache.http.conn.params.ConnRoutePNames; 070import org.apache.http.entity.ByteArrayEntity; 071import org.apache.http.impl.client.DefaultHttpClient; 072import org.apache.http.params.HttpConnectionParams; 073import org.apache.http.params.HttpParams; 074import org.hl7.fhir.r4.formats.IParser; 075import org.hl7.fhir.r4.formats.IParser.OutputStyle; 076import org.hl7.fhir.r4.formats.JsonParser; 077import org.hl7.fhir.r4.formats.XmlParser; 078import org.hl7.fhir.r4.model.Bundle; 079import org.hl7.fhir.r4.model.OperationOutcome; 080import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; 081import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; 082import org.hl7.fhir.r4.model.Resource; 083import org.hl7.fhir.r4.model.ResourceType; 084import org.hl7.fhir.r4.utils.ResourceUtilities; 085 086/** 087 * Helper class handling lower level HTTP transport concerns. 088 * TODO Document methods. 089 * @author Claude Nanjo 090 */ 091public class ClientUtils { 092 093 public static final String DEFAULT_CHARSET = "UTF-8"; 094 public static final String HEADER_LOCATION = "location"; 095 096 private HttpHost proxy; 097 private int timeout = 5000; 098 private String username; 099 private String password; 100 101 102 public HttpHost getProxy() { 103 return proxy; 104 } 105 106 public void setProxy(HttpHost proxy) { 107 this.proxy = proxy; 108 } 109 110 public int getTimeout() { 111 return timeout; 112 } 113 114 public void setTimeout(int timeout) { 115 this.timeout = timeout; 116 } 117 118 public String getUsername() { 119 return username; 120 } 121 122 public void setUsername(String username) { 123 this.username = username; 124 } 125 126 public String getPassword() { 127 return password; 128 } 129 130 public void setPassword(String password) { 131 this.password = password; 132 } 133 134 public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat) { 135 HttpOptions options = new HttpOptions(optionsUri); 136 return issueResourceRequest(resourceFormat, options); 137 } 138 139 public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat) { 140 HttpGet httpget = new HttpGet(resourceUri); 141 return issueResourceRequest(resourceFormat, httpget); 142 } 143 144 public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers) { 145 HttpPut httpPut = new HttpPut(resourceUri); 146 return issueResourceRequest(resourceFormat, httpPut, payload, headers); 147 } 148 149 public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat) { 150 HttpPut httpPut = new HttpPut(resourceUri); 151 return issueResourceRequest(resourceFormat, httpPut, payload, null); 152 } 153 154 public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat, List<Header> headers) { 155 HttpPost httpPost = new HttpPost(resourceUri); 156 return issueResourceRequest(resourceFormat, httpPost, payload, headers); 157 } 158 159 160 public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload, String resourceFormat) { 161 return issuePostRequest(resourceUri, payload, resourceFormat, null); 162 } 163 164 public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) { 165 HttpGet httpget = new HttpGet(resourceUri); 166 configureFhirRequest(httpget, resourceFormat); 167 HttpResponse response = sendRequest(httpget); 168 return unmarshalReference(response, resourceFormat); 169 } 170 171 private void setAuth(HttpRequest httpget) { 172 if (password != null) { 173 try { 174 byte[] b = Base64.encodeBase64((username+":"+password).getBytes("ASCII")); 175 String b64 = new String(b, StandardCharsets.US_ASCII); 176 httpget.setHeader("Authorization", "Basic " + b64); 177 } catch (UnsupportedEncodingException e) { 178 } 179 } 180 } 181 182 public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat) { 183 HttpPost httpPost = new HttpPost(resourceUri); 184 configureFhirRequest(httpPost, resourceFormat); 185 HttpResponse response = sendPayload(httpPost, payload, proxy); 186 return unmarshalFeed(response, resourceFormat); 187 } 188 189 public boolean issueDeleteRequest(URI resourceUri) { 190 HttpDelete deleteRequest = new HttpDelete(resourceUri); 191 HttpResponse response = sendRequest(deleteRequest); 192 int responseStatusCode = response.getStatusLine().getStatusCode(); 193 boolean deletionSuccessful = false; 194 if(responseStatusCode == 204) { 195 deletionSuccessful = true; 196 } 197 return deletionSuccessful; 198 } 199 200 /*********************************************************** 201 * Request/Response Helper methods 202 ***********************************************************/ 203 204 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request) { 205 return issueResourceRequest(resourceFormat, request, null); 206 } 207 208 /** 209 * @param resourceFormat 210 * @param options 211 * @return 212 */ 213 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload) { 214 return issueResourceRequest(resourceFormat, request, payload, null); 215 } 216 217 /** 218 * @param resourceFormat 219 * @param options 220 * @return 221 */ 222 protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HttpUriRequest request, byte[] payload, List<Header> headers) { 223 configureFhirRequest(request, resourceFormat, headers); 224 HttpResponse response = null; 225 if(request instanceof HttpEntityEnclosingRequest && payload != null) { 226 response = sendPayload((HttpEntityEnclosingRequestBase)request, payload, proxy); 227 } else if (request instanceof HttpEntityEnclosingRequest && payload == null){ 228 throw new EFhirClientException("PUT and POST requests require a non-null payload"); 229 } else { 230 response = sendRequest(request); 231 } 232 T resource = unmarshalReference(response, resourceFormat); 233 return new ResourceRequest<T>(resource, response.getStatusLine().getStatusCode(), getLocationHeader(response)); 234 } 235 236 237 /** 238 * Method adds required request headers. 239 * TODO handle JSON request as well. 240 * 241 * @param request 242 */ 243 protected void configureFhirRequest(HttpRequest request, String format) { 244 configureFhirRequest(request, format, null); 245 } 246 247 /** 248 * Method adds required request headers. 249 * TODO handle JSON request as well. 250 * 251 * @param request 252 */ 253 protected void configureFhirRequest(HttpRequest request, String format, List<Header> headers) { 254 request.addHeader("User-Agent", "Java FHIR Client for FHIR"); 255 256 if (format != null) { 257 request.addHeader("Accept",format); 258 request.addHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET); 259 } 260 request.addHeader("Accept-Charset", DEFAULT_CHARSET); 261 if(headers != null) { 262 for(Header header : headers) { 263 request.addHeader(header); 264 } 265 } 266 setAuth(request); 267 } 268 269 /** 270 * Method posts request payload 271 * 272 * @param request 273 * @param payload 274 * @return 275 */ 276 protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload, HttpHost proxy) { 277 HttpResponse response = null; 278 try { 279 HttpClient httpclient = new DefaultHttpClient(); 280 if(proxy != null) { 281 httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); 282 } 283 request.setEntity(new ByteArrayEntity(payload)); 284 response = httpclient.execute(request); 285 } catch(IOException ioe) { 286 throw new EFhirClientException("Error sending HTTP Post/Put Payload", ioe); 287 } 288 return response; 289 } 290 291 /** 292 * 293 * @param request 294 * @param payload 295 * @return 296 */ 297 protected HttpResponse sendRequest(HttpUriRequest request) { 298 HttpResponse response = null; 299 try { 300 HttpClient httpclient = new DefaultHttpClient(); 301 302 HttpParams params = httpclient.getParams(); 303 HttpConnectionParams.setConnectionTimeout(params, timeout); 304 HttpConnectionParams.setSoTimeout(params, timeout); 305 if(proxy != null) { 306 httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); 307 } 308 response = httpclient.execute(request); 309 } catch(IOException ioe) { 310 throw new EFhirClientException("Error sending Http Request: "+ioe.getMessage(), ioe); 311 } 312 return response; 313 } 314 315 /** 316 * Unmarshals a resource from the response stream. 317 * 318 * @param response 319 * @return 320 */ 321 @SuppressWarnings("unchecked") 322 protected <T extends Resource> T unmarshalReference(HttpResponse response, String format) { 323 T resource = null; 324 OperationOutcome error = null; 325 InputStream instream = null; 326 HttpEntity entity = response.getEntity(); 327 if (entity != null && entity.getContentLength() > 0) { 328 try { 329 instream = entity.getContent(); 330 // System.out.println(writeInputStreamAsString(instream)); 331 resource = (T)getParser(format).parse(instream); 332 if (resource instanceof OperationOutcome && hasError((OperationOutcome)resource)) { 333 error = (OperationOutcome) resource; 334 } 335 } catch(IOException ioe) { 336 throw new EFhirClientException("Error unmarshalling entity from Http Response: "+ioe.getMessage(), ioe); 337 } catch(Exception e) { 338 throw new EFhirClientException("Error parsing response message: "+e.getMessage(), e); 339 } finally { 340 try{instream.close();}catch(IOException ioe){/* TODO log error */} 341 } 342 } 343 if(error != null) { 344 throw new EFhirClientException("Error unmarshalling resource: "+ResourceUtilities.getErrorDescription(error), error); 345 } 346 return resource; 347 } 348 349 /** 350 * Unmarshals Bundle from response stream. 351 * 352 * @param response 353 * @return 354 */ 355 protected Bundle unmarshalFeed(HttpResponse response, String format) { 356 Bundle feed = null; 357 InputStream instream = null; 358 HttpEntity entity = response.getEntity(); 359 String contentType = response.getHeaders("Content-Type")[0].getValue(); 360 OperationOutcome error = null; 361 try { 362 if (entity != null) { 363 instream = entity.getContent(); 364 if(contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) { 365 // error = (OperationOutcome)getParser(ResourceFormat.RESOURCE_XML.getHeader()).parse(instream); 366 // } else { 367 Resource rf = getParser(format).parse(instream); 368 if (rf instanceof Bundle) 369 feed = (Bundle) rf; 370 else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) { 371 error = (OperationOutcome) rf; 372 } else { 373 throw new EFhirClientException("Error unmarshalling feed from Http Response: a resource was returned instead"); 374 } 375 } 376 instream.close(); 377 } 378 } catch(IOException ioe) { 379 throw new EFhirClientException("Error unmarshalling feed from Http Response", ioe); 380 } catch(Exception e) { 381 throw new EFhirClientException("Error parsing response message", e); 382 } finally { 383 try{instream.close();}catch(IOException ioe){/* TODO log error */} 384 } 385 if(error != null) { 386 throw new EFhirClientException("Error unmarshalling feed: "+ResourceUtilities.getErrorDescription(error), error); 387 } 388 return feed; 389 } 390 391 private boolean hasError(OperationOutcome oo) { 392 for (OperationOutcomeIssueComponent t : oo.getIssue()) 393 if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL) 394 return true; 395 return false; 396 } 397 398 protected String getLocationHeader(HttpResponse response) { 399 String location = null; 400 if(response.getHeaders("location").length > 0) {//TODO Distinguish between both cases if necessary 401 location = response.getHeaders("location")[0].getValue(); 402 } else if(response.getHeaders("content-location").length > 0) { 403 location = response.getHeaders("content-location")[0].getValue(); 404 } 405 return location; 406 } 407 408 409 /***************************************************************** 410 * Client connection methods 411 * ***************************************************************/ 412 413 public HttpURLConnection buildConnection(URI baseServiceUri, String tail) { 414 try { 415 HttpURLConnection client = (HttpURLConnection) baseServiceUri.resolve(tail).toURL().openConnection(); 416 return client; 417 } catch(MalformedURLException mue) { 418 throw new EFhirClientException("Invalid Service URL", mue); 419 } catch(IOException ioe) { 420 throw new EFhirClientException("Unable to establish connection to server: " + baseServiceUri.toString() + tail, ioe); 421 } 422 } 423 424 public HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) { 425 return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id)); 426 } 427 428 /****************************************************************** 429 * Other general helper methods 430 * ****************************************************************/ 431 432 433 public <T extends Resource> byte[] getResourceAsByteArray(T resource, boolean pretty, boolean isJson) { 434 ByteArrayOutputStream baos = null; 435 byte[] byteArray = null; 436 try { 437 baos = new ByteArrayOutputStream(); 438 IParser parser = null; 439 if(isJson) { 440 parser = new JsonParser(); 441 } else { 442 parser = new XmlParser(); 443 } 444 parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); 445 parser.compose(baos, resource); 446 baos.close(); 447 byteArray = baos.toByteArray(); 448 baos.close(); 449 } catch (Exception e) { 450 try{ 451 baos.close(); 452 }catch(Exception ex) { 453 throw new EFhirClientException("Error closing output stream", ex); 454 } 455 throw new EFhirClientException("Error converting output stream to byte array", e); 456 } 457 return byteArray; 458 } 459 460 public byte[] getFeedAsByteArray(Bundle feed, boolean pretty, boolean isJson) { 461 ByteArrayOutputStream baos = null; 462 byte[] byteArray = null; 463 try { 464 baos = new ByteArrayOutputStream(); 465 IParser parser = null; 466 if(isJson) { 467 parser = new JsonParser(); 468 } else { 469 parser = new XmlParser(); 470 } 471 parser.setOutputStyle(pretty ? OutputStyle.PRETTY : OutputStyle.NORMAL); 472 parser.compose(baos, feed); 473 baos.close(); 474 byteArray = baos.toByteArray(); 475 baos.close(); 476 } catch (Exception e) { 477 try{ 478 baos.close(); 479 }catch(Exception ex) { 480 throw new EFhirClientException("Error closing output stream", ex); 481 } 482 throw new EFhirClientException("Error converting output stream to byte array", e); 483 } 484 return byteArray; 485 } 486 487 public Calendar getLastModifiedResponseHeaderAsCalendarObject(URLConnection serverConnection) { 488 String dateTime = null; 489 try { 490 dateTime = serverConnection.getHeaderField("Last-Modified"); 491 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); 492 Date lastModifiedTimestamp = format.parse(dateTime); 493 Calendar calendar=Calendar.getInstance(); 494 calendar.setTime(lastModifiedTimestamp); 495 return calendar; 496 } catch(ParseException pe) { 497 throw new EFhirClientException("Error parsing Last-Modified response header " + dateTime, pe); 498 } 499 } 500 501 protected IParser getParser(String format) { 502 if(StringUtils.isBlank(format)) { 503 format = ResourceFormat.RESOURCE_XML.getHeader(); 504 } 505 if(format.equalsIgnoreCase("json") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) { 506 return new JsonParser(); 507 } else if(format.equalsIgnoreCase("xml") || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader()) || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) { 508 return new XmlParser(); 509 } else { 510 throw new EFhirClientException("Invalid format: " + format); 511 } 512 } 513 514 /** 515 * Used for debugging 516 * 517 * @param instream 518 * @return 519 */ 520 protected String writeInputStreamAsString(InputStream instream) { 521 String value = null; 522 try { 523 value = IOUtils.toString(instream, "UTF-8"); 524 System.out.println(value); 525 526 } catch(IOException ioe) { 527 //Do nothing 528 } 529 return value; 530 } 531 532 public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName, Resource resource, String resourceFormat) throws IOException { 533 HttpPost httppost = new HttpPost(resourceUri); 534 String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy"; 535 httppost.addHeader("Content-Type", "multipart/form-data; boundary="+boundary); 536 httppost.addHeader("Accept", resourceFormat); 537 configureFhirRequest(httppost, null); 538 HttpResponse response = sendPayload(httppost, encodeFormSubmission(parameters, resourceName, resource, boundary)); 539 return unmarshalFeed(response, resourceFormat); 540 } 541 542 private byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource, String boundary) throws IOException { 543 ByteArrayOutputStream b = new ByteArrayOutputStream(); 544 OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8"); 545 for (String name : parameters.keySet()) { 546 w.write("--"); 547 w.write(boundary); 548 w.write("\r\nContent-Disposition: form-data; name=\""+name+"\"\r\n\r\n"); 549 w.write(parameters.get(name)+"\r\n"); 550 } 551 w.write("--"); 552 w.write(boundary); 553 w.write("\r\nContent-Disposition: form-data; name=\""+resourceName+"\"\r\n\r\n"); 554 w.close(); 555 JsonParser json = new JsonParser(); 556 json.setOutputStyle(OutputStyle.NORMAL); 557 json.compose(b, resource); 558 b.close(); 559 w = new OutputStreamWriter(b, "UTF-8"); 560 w.write("\r\n--"); 561 w.write(boundary); 562 w.write("--"); 563 w.close(); 564 return b.toByteArray(); 565 } 566 567 /** 568 * Method posts request payload 569 * 570 * @param request 571 * @param payload 572 * @return 573 */ 574 protected HttpResponse sendPayload(HttpEntityEnclosingRequestBase request, byte[] payload) { 575 HttpResponse response = null; 576 try { 577 HttpClient httpclient = new DefaultHttpClient(); 578 request.setEntity(new ByteArrayEntity(payload)); 579 response = httpclient.execute(request); 580 } catch(IOException ioe) { 581 throw new EFhirClientException("Error sending HTTP Post/Put Payload: "+ioe.getMessage(), ioe); 582 } 583 return response; 584 } 585 586 587}