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