001package org.hl7.fhir.dstu2.utils.client;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009
010 * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012 * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015 * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029
030 */
031
032import java.io.ByteArrayOutputStream;
033import java.io.IOException;
034import java.io.InputStream;
035import java.io.OutputStreamWriter;
036import java.net.HttpURLConnection;
037import java.net.MalformedURLException;
038import java.net.URI;
039import java.net.URLConnection;
040import java.text.ParseException;
041import java.text.SimpleDateFormat;
042import java.util.*;
043import java.util.concurrent.TimeUnit;
044
045import lombok.Getter;
046import lombok.Setter;
047
048import org.apache.commons.io.IOUtils;
049import org.apache.commons.lang3.StringUtils;
050
051import org.hl7.fhir.dstu2.formats.IParser;
052import org.hl7.fhir.dstu2.formats.IParser.OutputStyle;
053import org.hl7.fhir.dstu2.formats.JsonParser;
054import org.hl7.fhir.dstu2.formats.XmlParser;
055import org.hl7.fhir.dstu2.model.Bundle;
056import org.hl7.fhir.dstu2.model.OperationOutcome;
057import org.hl7.fhir.dstu2.model.OperationOutcome.IssueSeverity;
058import org.hl7.fhir.dstu2.model.OperationOutcome.OperationOutcomeIssueComponent;
059import org.hl7.fhir.dstu2.model.Resource;
060import org.hl7.fhir.dstu2.model.ResourceType;
061import org.hl7.fhir.dstu2.utils.ResourceUtilities;
062import org.hl7.fhir.exceptions.FHIRException;
063import org.hl7.fhir.utilities.MimeType;
064import org.hl7.fhir.utilities.ToolingClientLogger;
065import org.hl7.fhir.utilities.Utilities;
066import org.hl7.fhir.utilities.http.*;
067import org.hl7.fhir.utilities.settings.FhirSettings;
068
069import javax.annotation.Nonnull;
070
071/**
072 * Helper class handling lower level HTTP transport concerns. TODO Document
073 * methods.
074 * 
075 * @author Claude Nanjo
076 */
077@Deprecated
078public class ClientUtils {
079  protected static final String LOCATION_HEADER = "location";
080  protected static final String CONTENT_LOCATION_HEADER = "content-location";
081  public static final String DEFAULT_CHARSET = "UTF-8";
082
083  private static boolean debugging = false;
084
085  @Getter
086  @Setter
087  private int timeout = 5000;
088
089  @Setter
090  @Getter
091  private ToolingClientLogger logger;
092
093  @Setter
094  @Getter
095  private int retryCount;
096
097  @Getter
098  @Setter
099  private String userAgent;
100  @Setter
101  private String acceptLanguage;
102  @Setter
103  private String contentLanguage;
104  private final TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
105
106  protected ManagedFhirWebAccessor getManagedWebAccessor() {
107    return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
108  }
109
110  public <T extends Resource> ResourceRequest<T> issueOptionsRequest(URI optionsUri, String resourceFormat,
111      int timeoutLoading) {
112    if (FhirSettings.isProhibitNetworkAccess()) {
113      throw new FHIRException("Network Access is prohibited in this context");
114    }
115
116    HTTPRequest httpRequest = new HTTPRequest()
117      .withMethod(HTTPRequest.HttpMethod.OPTIONS)
118      .withUrl(optionsUri.toString());
119    return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading);
120  }
121
122  public <T extends Resource> ResourceRequest<T> issueGetResourceRequest(URI resourceUri, String resourceFormat,
123      int timeoutLoading) {
124    if (FhirSettings.isProhibitNetworkAccess()) {
125      throw new FHIRException("Network Access is prohibited in this context");
126    }
127
128    HTTPRequest httpRequest = new HTTPRequest()
129      .withMethod(HTTPRequest.HttpMethod.GET)
130      .withUrl(resourceUri.toString());
131    return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading);
132  }
133
134  public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
135      Iterable<HTTPHeader> headers, int timeoutLoading) {
136    if (FhirSettings.isProhibitNetworkAccess()) {
137      throw new FHIRException("Network Access is prohibited in this context");
138    }
139    HTTPRequest httpRequest = new HTTPRequest()
140      .withMethod(HTTPRequest.HttpMethod.PUT)
141      .withUrl(resourceUri.toString())
142      .withBody(payload);
143    return issueResourceRequest(resourceFormat, httpRequest, headers, timeoutLoading);
144  }
145
146  public <T extends Resource> ResourceRequest<T> issuePutRequest(URI resourceUri, byte[] payload, String resourceFormat,
147      int timeoutLoading) {
148    if (FhirSettings.isProhibitNetworkAccess()) {
149      throw new FHIRException("Network Access is prohibited in this context");
150    }
151
152    HTTPRequest httpRequest = new HTTPRequest()
153      .withMethod(HTTPRequest.HttpMethod.PUT)
154      .withUrl(resourceUri.toString())
155      .withBody(payload);
156    return issueResourceRequest(resourceFormat, httpRequest, timeoutLoading);
157  }
158
159  public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload,
160      String resourceFormat, Iterable<HTTPHeader> headers, int timeoutLoading) {
161    if (FhirSettings.isProhibitNetworkAccess()) {
162      throw new FHIRException("Network Access is prohibited in this context");
163    }
164
165    HTTPRequest httpRequest = new HTTPRequest()
166      .withMethod(HTTPRequest.HttpMethod.POST)
167      .withUrl(resourceUri.toString())
168      .withBody(payload);
169    return issueResourceRequest(resourceFormat, httpRequest, headers, timeoutLoading);
170  }
171
172  public <T extends Resource> ResourceRequest<T> issuePostRequest(URI resourceUri, byte[] payload,
173      String resourceFormat, int timeoutLoading) {
174    return issuePostRequest(resourceUri, payload, resourceFormat, null, timeoutLoading);
175  }
176
177  public Bundle issueGetFeedRequest(URI resourceUri, String resourceFormat) {
178    if (FhirSettings.isProhibitNetworkAccess()) {
179      throw new FHIRException("Network Access is prohibited in this context");
180    }
181
182    HTTPRequest httpRequest = new HTTPRequest()
183      .withMethod(HTTPRequest.HttpMethod.GET)
184      .withUrl(resourceUri.toString());
185    Iterable<HTTPHeader> headers = getFhirHeaders(httpRequest, resourceFormat);
186    HTTPResult response = sendRequest(httpRequest.withHeaders(headers));
187    return unmarshalReference(response, resourceFormat);
188  }
189
190  public Bundle postBatchRequest(URI resourceUri, byte[] payload, String resourceFormat, int timeoutLoading) {
191    if (FhirSettings.isProhibitNetworkAccess()) {
192      throw new FHIRException("Network Access is prohibited in this context");
193    }
194
195    HTTPRequest httpRequest = new HTTPRequest()
196      .withMethod(HTTPRequest.HttpMethod.POST)
197      .withUrl(resourceUri.toString())
198      .withBody(payload);
199    Iterable<HTTPHeader> headers =  getFhirHeaders(httpRequest, resourceFormat);
200    HTTPResult response = sendPayload(httpRequest.withHeaders(headers));
201    return unmarshalFeed(response, resourceFormat);
202  }
203
204  public boolean issueDeleteRequest(URI resourceUri) {
205    if (FhirSettings.isProhibitNetworkAccess()) {
206      throw new FHIRException("Network Access is prohibited in this context");
207    }
208
209    HTTPRequest request = new HTTPRequest()
210      .withMethod(HTTPRequest.HttpMethod.DELETE)
211      .withUrl(resourceUri.toString());
212    HTTPResult response = sendRequest(request);
213    int responseStatusCode = response.getCode();
214    boolean deletionSuccessful = false;
215    if (responseStatusCode == 204) {
216      deletionSuccessful = true;
217    }
218    return deletionSuccessful;
219  }
220
221  /***********************************************************
222   * Request/Response Helper methods
223   ***********************************************************/
224
225  protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HTTPRequest request,
226      int timeoutLoading) {
227    return issueResourceRequest(resourceFormat, request, Collections.emptyList(), timeoutLoading);
228  }
229  /**
230   * Issue a resource request.
231   * @param resourceFormat the expected FHIR format
232   * @param request the request to be sent
233   * @param headers any additional headers to add
234   *
235   * @return A ResourceRequest object containing the requested resource
236   */
237  protected <T extends Resource> ResourceRequest<T> issueResourceRequest(String resourceFormat, HTTPRequest request,
238                                                                         @Nonnull Iterable<HTTPHeader> headers, int timeoutLoading) {
239    if (FhirSettings.isProhibitNetworkAccess()) {
240      throw new FHIRException("Network Access is prohibited in this context");
241    }
242    Iterable<HTTPHeader> configuredHeaders = getFhirHeaders(request, resourceFormat, headers);
243    try {
244
245      HTTPResult response = getManagedWebAccessor().httpCall(request.withHeaders(configuredHeaders));
246      T resource = unmarshalReference(response, resourceFormat);
247      return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
248    } catch (IOException ioe) {
249      throw new EFhirClientException("Error sending HTTP Post/Put Payload to " + "??" + ": " + ioe.getMessage(),
250        ioe);
251    }
252  }
253
254  /**
255   * Get required headers for FHIR requests.
256   * 
257   * @param httpRequest the request
258   * @param format the expected format
259   */
260  protected Iterable<HTTPHeader> getFhirHeaders(HTTPRequest httpRequest, String format) {
261    return getFhirHeaders(httpRequest, format, null);
262  }
263
264  /**
265   * Get required headers for FHIR requests.
266   * 
267   * @param httpRequest the request
268   * @param format the expected format
269   * @param headers any additional headers to add
270   */
271  protected Iterable<HTTPHeader> getFhirHeaders(HTTPRequest httpRequest, String format, Iterable<HTTPHeader> headers) {
272    List<HTTPHeader> configuredHeaders = new ArrayList<>();
273    if (!Utilities.noString(userAgent)) {
274      configuredHeaders.add(new HTTPHeader("User-Agent", userAgent));
275    }
276    if (!Utilities.noString(acceptLanguage)) {
277      configuredHeaders.add(new HTTPHeader("Accept-Language", acceptLanguage));
278    }
279    if (!Utilities.noString(contentLanguage)) {
280      configuredHeaders.add(new HTTPHeader("Content-Language", acceptLanguage));
281    }
282
283    Iterable<HTTPHeader> resourceFormatHeaders = getResourceFormatHeaders(httpRequest, format);
284    resourceFormatHeaders.forEach(configuredHeaders::add);
285
286    if (headers != null) {
287      headers.forEach(configuredHeaders::add);
288    }
289    return configuredHeaders;
290  }
291
292  protected static List<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
293    List<HTTPHeader> headers = new ArrayList<>();
294    headers.add(new HTTPHeader("Accept", format));
295    if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
296      || httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
297      || httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
298    ) {
299      headers.add(new HTTPHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET));
300    }
301    return headers;
302  }
303
304  /**
305   * 
306   * @param request The request to be sent
307   * @return The response from the server
308   */
309  protected HTTPResult sendRequest(HTTPRequest request) {
310    if (FhirSettings.isProhibitNetworkAccess()) {
311      throw new FHIRException("Network Access is prohibited in this context");
312    }
313    HTTPResult response = null;
314    try {
315
316      response = getManagedWebAccessor().httpCall(request);
317      return response;
318    } catch (IOException ioe) {
319      if (ClientUtils.debugging) {
320        ioe.printStackTrace();
321      }
322      throw new EFhirClientException("Error sending Http Request: " + ioe.getMessage(), ioe);
323    }
324  }
325
326  /**
327   * Unmarshals a resource from the response stream.
328   * 
329   * @param response The response from the server
330   * @return The unmarshalled resource
331   */
332  @SuppressWarnings("unchecked")
333  protected <T extends Resource> T unmarshalReference(HTTPResult response, String format) {
334    T resource = null;
335    OperationOutcome error = null;
336    if (response.getContent() != null) {
337      try {
338        resource = (T) getParser(format).parse(response.getContent());
339        if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) {
340          error = (OperationOutcome) resource;
341        }
342      } catch (IOException ioe) {
343        throw new EFhirClientException("Error reading Http Response: " + ioe.getMessage(), ioe);
344      } catch (Exception e) {
345        throw new EFhirClientException("Error parsing response message: " + e.getMessage(), e);
346      }
347    }
348    if (error != null) {
349      throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error);
350    }
351    return resource;
352  }
353
354  /**
355   * Unmarshals Bundle from response stream.
356   * 
357   * @param response The response from the server
358   * @return The unmarshalled Bundle
359   */
360  protected Bundle unmarshalFeed(HTTPResult response, String format) {
361    Bundle feed = null;
362
363    String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
364    OperationOutcome error = null;
365    try {
366      if (response.getContent() != null) {
367        if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains("text/xml+fhir")) {
368          Resource rf = getParser(format).parse(response.getContent());
369          if (rf instanceof Bundle)
370            feed = (Bundle) rf;
371          else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) {
372            error = (OperationOutcome) rf;
373          } else {
374            throw new EFhirClientException("Error reading server response: a resource was returned instead");
375          }
376        }
377      }
378    } catch (IOException ioe) {
379      throw new EFhirClientException("Error reading Http Response", ioe);
380    } catch (Exception e) {
381      throw new EFhirClientException("Error parsing response message", e);
382    }
383    if (error != null) {
384      throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error);
385    }
386    return feed;
387  }
388
389  protected boolean hasError(OperationOutcome oo) {
390    for (OperationOutcomeIssueComponent t : oo.getIssue())
391      if (t.getSeverity() == IssueSeverity.ERROR || t.getSeverity() == IssueSeverity.FATAL)
392        return true;
393    return false;
394  }
395
396  protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
397    String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
398
399    if (locationHeader != null) {
400      return locationHeader;
401    }
402    return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
403  }
404
405  /*****************************************************************
406   * Client connection methods
407   ***************************************************************/
408
409  public HttpURLConnection buildConnection(URI baseServiceUri, String tail) {
410    if (FhirSettings.isProhibitNetworkAccess()) {
411      throw new FHIRException("Network Access is prohibited in this context");
412    }
413
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,
421          ioe);
422    }
423  }
424
425  public HttpURLConnection buildConnection(URI baseServiceUri, ResourceType resourceType, String id) {
426    return buildConnection(baseServiceUri, ResourceAddress.buildRelativePathFromResourceType(resourceType, id));
427  }
428
429  /******************************************************************
430   * Other general helper methods
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", new Locale("en", "US"));
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    MimeType mm = new MimeType(format);
506    if (mm.getBase().equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())
507        || format.equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) {
508      return new JsonParser();
509    } else if (mm.getBase().equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())
510        || format.equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) {
511      return new XmlParser();
512    } else {
513      throw new EFhirClientException("Invalid format: " + format);
514    }
515  }
516
517  public Bundle issuePostFeedRequest(URI resourceUri, Map<String, String> parameters, String resourceName,
518      Resource resource, String resourceFormat) throws IOException {
519
520    HTTPRequest httpRequest = new HTTPRequest()
521      .withMethod(HTTPRequest.HttpMethod.POST)
522      .withUrl(resourceUri.toString());
523    String boundary = "----WebKitFormBoundarykbMUo6H8QaUnYtRy";
524  List<HTTPHeader> headers = new ArrayList<>();
525    headers.add(new HTTPHeader("Content-Type", "multipart/form-data; boundary=" + boundary));
526    headers.add(new HTTPHeader("Accept", resourceFormat));
527    this.getFhirHeaders(httpRequest, null).forEach(headers::add);
528
529    HTTPResult response = sendPayload(httpRequest.withBody(encodeFormSubmission(parameters, resourceName, resource, boundary)).withHeaders(headers));
530    return unmarshalFeed(response, resourceFormat);
531  }
532
533  private byte[] encodeFormSubmission(Map<String, String> parameters, String resourceName, Resource resource,
534      String boundary) throws IOException {
535    ByteArrayOutputStream b = new ByteArrayOutputStream();
536    OutputStreamWriter w = new OutputStreamWriter(b, "UTF-8");
537    for (String name : parameters.keySet()) {
538      w.write("--");
539      w.write(boundary);
540      w.write("\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
541      w.write(parameters.get(name) + "\r\n");
542    }
543    w.write("--");
544    w.write(boundary);
545    w.write("\r\nContent-Disposition: form-data; name=\"" + resourceName + "\"\r\n\r\n");
546    w.close();
547    JsonParser json = new JsonParser();
548    json.setOutputStyle(OutputStyle.NORMAL);
549    json.compose(b, resource);
550    b.close();
551    w = new OutputStreamWriter(b, "UTF-8");
552    w.write("\r\n--");
553    w.write(boundary);
554    w.write("--");
555    w.close();
556    return b.toByteArray();
557  }
558
559  /**
560   * Send an HTTP Post/Put Payload
561   * 
562   * @param request The request to be sent
563   * @return The response from the server
564   */
565  protected HTTPResult sendPayload(HTTPRequest request) {
566    HTTPResult response = null;
567    try {
568
569      response = getManagedWebAccessor().httpCall(request);
570    } catch (IOException ioe) {
571      throw new EFhirClientException("Error sending HTTP Post/Put Payload: " + ioe.getMessage(), ioe);
572    }
573    return response;
574  }
575
576  /**
577   * Used for debugging
578   * 
579   * @param instream
580   * @return
581   */
582  protected String writeInputStreamAsString(InputStream instream) {
583    String value = null;
584    try {
585      value = IOUtils.toString(instream, "UTF-8");
586      System.out.println(value);
587
588    } catch (IOException ioe) {
589      // Do nothing
590    }
591    return value;
592  }
593
594
595}