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