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}