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