001package org.hl7.fhir.dstu3.utils.client.network;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.List;
006import java.util.concurrent.TimeUnit;
007
008import lombok.Getter;
009import lombok.Setter;
010
011import org.apache.commons.lang3.StringUtils;
012import org.hl7.fhir.dstu3.formats.IParser;
013import org.hl7.fhir.dstu3.formats.JsonParser;
014import org.hl7.fhir.dstu3.formats.XmlParser;
015import org.hl7.fhir.dstu3.model.Bundle;
016import org.hl7.fhir.dstu3.model.OperationOutcome;
017import org.hl7.fhir.dstu3.model.Resource;
018import org.hl7.fhir.dstu3.utils.ResourceUtilities;
019import org.hl7.fhir.dstu3.utils.client.EFhirClientException;
020import org.hl7.fhir.dstu3.utils.client.ResourceFormat;
021import org.hl7.fhir.utilities.MimeType;
022import org.hl7.fhir.utilities.ToolingClientLogger;
023import org.hl7.fhir.utilities.http.*;
024
025
026public class FhirRequestBuilder {
027
028  protected static final String LOCATION_HEADER = "location";
029  protected static final String CONTENT_LOCATION_HEADER = "content-location";
030  protected static final String DEFAULT_CHARSET = "UTF-8";
031
032  private final HTTPRequest httpRequest;
033  private String resourceFormat = null;
034  private Iterable<HTTPHeader> headers = null;
035  private String message = null;
036  private int retryCount = 1;
037  /**
038   * The timeout quantity. Used in combination with {@link FhirRequestBuilder#timeoutUnit}.
039   */
040  private long timeout = 5000;
041  /**
042   * Time unit for {@link FhirRequestBuilder#timeout}.
043   */
044  private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;
045  /**
046   * {@link ToolingClientLogger} for log output.
047   */
048  @Getter @Setter
049  private ToolingClientLogger logger = null;
050  private String source;
051
052  public FhirRequestBuilder(HTTPRequest httpRequest, String source) {
053    this.httpRequest = httpRequest;
054    this.source = source;
055  }
056
057  /**
058   * Adds necessary default headers, formatting headers, and any passed in {@link HTTPHeader}s to the passed in
059   * {@link HTTPRequest}
060   *
061   * @param request {@link HTTPRequest} to add headers to.
062   * @param format  Expected {@link Resource} format.
063   * @param headers Any additional {@link HTTPHeader}s to add to the request.
064   */
065  protected static HTTPRequest formatHeaders(HTTPRequest request, String format, Iterable<HTTPHeader> headers) {
066    List<HTTPHeader> allHeaders = new ArrayList<>();
067    request.getHeaders().forEach(allHeaders::add);
068
069    if (format != null) getResourceFormatHeaders(request, format).forEach(allHeaders::add);
070    if (headers != null) headers.forEach(allHeaders::add);
071    return request.withHeaders(allHeaders);
072  }
073  protected static Iterable<HTTPHeader> getResourceFormatHeaders(HTTPRequest httpRequest, String format) {
074    List<HTTPHeader> headers = new ArrayList<>();
075    headers.add(new HTTPHeader("Accept", format));
076    if (httpRequest.getMethod() == HTTPRequest.HttpMethod.PUT
077      || httpRequest.getMethod() == HTTPRequest.HttpMethod.POST
078      || httpRequest.getMethod() == HTTPRequest.HttpMethod.PATCH
079    ) {
080      headers.add(new HTTPHeader("Content-Type", format + ";charset=" + DEFAULT_CHARSET));
081    }
082    return headers;
083  }
084
085  /**
086   * Returns true if any of the {@link org.hl7.fhir.dstu3.model.OperationOutcome.OperationOutcomeIssueComponent} within the
087   * provided {@link OperationOutcome} have an {@link org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity} of
088   * {@link org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity#ERROR} or
089   * {@link org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity#FATAL}
090   *
091   * @param oo {@link OperationOutcome} to evaluate.
092   * @return {@link Boolean#TRUE} if an error exists.
093   */
094  protected static boolean hasError(OperationOutcome oo) {
095    return (oo.getIssue().stream()
096      .anyMatch(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR
097        || issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL));
098  }
099
100  /**
101   * Extracts the 'location' header from the passed {@link Iterable<HTTPHeader>}. If no
102   * value for 'location' exists, the value for 'content-location' is returned. If
103   * neither header exists, we return null.
104   *
105   * @param headers {@link HTTPHeader} to evaluate
106   * @return {@link String} header value, or null if no location headers are set.
107   */
108  protected static String getLocationHeader(Iterable<HTTPHeader> headers) {
109    String locationHeader = HTTPHeaderUtil.getSingleHeader(headers, LOCATION_HEADER);
110
111    if (locationHeader != null) {
112      return locationHeader;
113    }
114    return HTTPHeaderUtil.getSingleHeader(headers, CONTENT_LOCATION_HEADER);
115  }
116
117  protected ManagedFhirWebAccessor getManagedWebAccessor() {
118    return ManagedWebAccess.fhirAccessor().withRetries(retryCount).withTimeout(timeout, timeoutUnit).withLogger(logger);
119  }
120
121  public FhirRequestBuilder withResourceFormat(String resourceFormat) {
122    this.resourceFormat = resourceFormat;
123    return this;
124  }
125
126  public FhirRequestBuilder withHeaders(Iterable<HTTPHeader> headers) {
127    this.headers = headers;
128    return this;
129  }
130
131  public FhirRequestBuilder withMessage(String message) {
132    this.message = message;
133    return this;
134  }
135
136  public FhirRequestBuilder withRetryCount(int retryCount) {
137    this.retryCount = retryCount;
138    return this;
139  }
140
141  public FhirRequestBuilder withLogger(ToolingClientLogger logger) {
142    this.logger = logger;
143    return this;
144  }
145
146  public FhirRequestBuilder withTimeout(long timeout, TimeUnit unit) {
147    this.timeout = timeout;
148    this.timeoutUnit = unit;
149    return this;
150  }
151
152  public <T extends Resource> ResourceRequest<T> execute() throws IOException {
153    HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, headers);
154    HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
155    T resource = unmarshalReference(response, resourceFormat);
156    return new ResourceRequest<T>(resource, response.getCode(), getLocationHeader(response.getHeaders()));
157  }
158
159  public Bundle executeAsBatch() throws IOException {
160    HTTPRequest requestWithHeaders = formatHeaders(httpRequest, resourceFormat, null);
161    HTTPResult response = getManagedWebAccessor().httpCall(requestWithHeaders);
162    return unmarshalFeed(response, resourceFormat);
163  }
164
165  /**
166   * Unmarshalls a resource from the response stream.
167   */
168  @SuppressWarnings("unchecked")
169  protected <T extends Resource> T unmarshalReference(HTTPResult response, String format) {
170    T resource = null;
171    OperationOutcome error = null;
172
173    if (response.getContent() != null) {
174      try {
175        byte[] body = response.getContent();
176
177        resource = (T) getParser(format).parse(body);
178        if (resource instanceof OperationOutcome && hasError((OperationOutcome) resource)) {
179          error = (OperationOutcome) resource;
180        }
181      } catch (IOException ioe) {
182        throw new EFhirClientException("Error reading Http Response from "+source+": " + ioe.getMessage(), ioe);
183      } catch (Exception e) {
184        throw new EFhirClientException("Error parsing response message from "+source+": " + e.getMessage(), e);
185      }
186    }
187
188    if (error != null) {
189      throw new EFhirClientException("Error from server: " + ResourceUtilities.getErrorDescription(error), error);
190    }
191
192    return resource;
193  }
194
195  /**
196   * Unmarshalls Bundle from response stream.
197   */
198  protected Bundle unmarshalFeed(HTTPResult response, String format) {
199    Bundle feed = null;
200    OperationOutcome error = null;
201    try {
202      byte[] body = response.getContent();
203
204      String contentType = HTTPHeaderUtil.getSingleHeader(response.getHeaders(), "Content-Type");
205      if (body != null) {
206        if (contentType.contains(ResourceFormat.RESOURCE_XML.getHeader()) || contentType.contains(ResourceFormat.RESOURCE_JSON.getHeader()) || contentType.contains("text/xml+fhir")) {
207          Resource rf = getParser(format).parse(body);
208          if (rf instanceof Bundle)
209            feed = (Bundle) rf;
210          else if (rf instanceof OperationOutcome && hasError((OperationOutcome) rf)) {
211            error = (OperationOutcome) rf;
212          } else {
213            throw new EFhirClientException("Error reading server response: a resource was returned instead");
214          }
215        }
216      }
217    } catch (IOException ioe) {
218      throw new EFhirClientException("Error reading Http Response from "+source+": "+ioe.getMessage(), ioe);
219    } catch (Exception e) {
220      throw new EFhirClientException("Error parsing response message from "+source+":"+e.getMessage(), e);
221    }
222    if (error != null) {
223      throw new EFhirClientException("Error from "+source+": " + ResourceUtilities.getErrorDescription(error), error);
224    }
225    return feed;
226  }
227
228  /**
229   * Returns the appropriate parser based on the format type passed in. Defaults to XML parser if a blank format is
230   * provided...because reasons.
231   * <p/>
232   * Currently supports only "json" and "xml" formats.
233   *
234   * @param format One of "json" or "xml".
235   * @return {@link JsonParser} or {@link XmlParser}
236   */
237  protected IParser getParser(String format) {
238    if (StringUtils.isBlank(format)) {
239      format = ResourceFormat.RESOURCE_XML.getHeader();
240    }
241    MimeType mt = new MimeType(format);
242    if (mt.getBase().equalsIgnoreCase(ResourceFormat.RESOURCE_JSON.getHeader())) {
243      return new JsonParser();
244    } else if (mt.getBase().equalsIgnoreCase(ResourceFormat.RESOURCE_XML.getHeader())) {
245      return new XmlParser();
246    } else {
247      throw new EFhirClientException("Invalid format: " + format);
248    }
249  }
250}