001package org.hl7.fhir.dstu3.utils.client;
002
003import okhttp3.Headers;
004import okhttp3.internal.http2.Header;
005import org.hl7.fhir.dstu3.model.*;
006import org.hl7.fhir.exceptions.FHIRException;
007import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
008import org.hl7.fhir.dstu3.utils.client.network.ByteUtils;
009import org.hl7.fhir.dstu3.utils.client.network.Client;
010import org.hl7.fhir.dstu3.utils.client.network.ResourceRequest;
011import org.hl7.fhir.utilities.ToolingClientLogger;
012import org.hl7.fhir.utilities.Utilities;
013
014import java.io.IOException;
015import java.net.URI;
016import java.net.URISyntaxException;
017import java.util.ArrayList;
018import java.util.Base64;
019import java.util.HashMap;
020import java.util.Map;
021
022/**
023 * Very Simple RESTful client. This is purely for use in the standalone
024 * tools jar packages. It doesn't support many features, only what the tools
025 * need.
026 * <p>
027 * To use, initialize class and set base service URI as follows:
028 *
029 * <pre><code>
030 * FHIRSimpleClient fhirClient = new FHIRSimpleClient();
031 * fhirClient.initialize("http://my.fhir.domain/myServiceRoot");
032 * </code></pre>
033 * <p>
034 * Default Accept and Content-Type headers are application/fhir+xml and application/fhir+json.
035 * <p>
036 * These can be changed by invoking the following setter functions:
037 *
038 * <pre><code>
039 * setPreferredResourceFormat()
040 * setPreferredFeedFormat()
041 * </code></pre>
042 * <p>
043 * TODO Review all sad paths.
044 *
045 * @author Claude Nanjo
046 */
047public class FHIRToolingClient {
048
049  public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssK";
050  public static final String DATE_FORMAT = "yyyy-MM-dd";
051  public static final String hostKey = "http.proxyHost";
052  public static final String portKey = "http.proxyPort";
053
054  private static final int TIMEOUT_NORMAL = 1500;
055  private static final int TIMEOUT_OPERATION = 30000;
056  private static final int TIMEOUT_ENTRY = 500;
057  private static final int TIMEOUT_OPERATION_LONG = 60000;
058  private static final int TIMEOUT_OPERATION_EXPAND = 120000;
059
060  private String base;
061  private ResourceAddress resourceAddress;
062  private ResourceFormat preferredResourceFormat;
063  private int maxResultSetSize = -1;//_count
064  private CapabilityStatement capabilities;
065  private Client client = new Client();
066  private ArrayList<Header> headers = new ArrayList<>();
067  private String username;
068  private String password;
069  private String userAgent;
070
071  //Pass endpoint for client - URI
072  public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
073    preferredResourceFormat = ResourceFormat.RESOURCE_XML;
074    this.userAgent = userAgent;
075    initialize(baseServiceUrl);
076  }
077
078  public void initialize(String baseServiceUrl) throws URISyntaxException {
079    base = baseServiceUrl;
080    resourceAddress = new ResourceAddress(baseServiceUrl);
081    this.maxResultSetSize = -1;
082    checkCapabilities();
083  }
084
085  public Client getClient() {
086    return client;
087  }
088
089  public void setClient(Client client) {
090    this.client = client;
091  }
092
093  private void checkCapabilities() {
094    try {
095      capabilities = getCapabilitiesStatementQuick();
096    } catch (Throwable e) {
097    }
098  }
099
100  public String getPreferredResourceFormat() {
101    return preferredResourceFormat.getHeader();
102  }
103
104  public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
105    preferredResourceFormat = resourceFormat;
106  }
107
108  public int getMaximumRecordCount() {
109    return maxResultSetSize;
110  }
111
112  public void setMaximumRecordCount(int maxResultSetSize) {
113    this.maxResultSetSize = maxResultSetSize;
114  }
115
116  public Parameters getTerminologyCapabilities() {
117    Parameters capabilities = null;
118    try {
119      capabilities = (Parameters) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
120        getPreferredResourceFormat(),
121        generateHeaders(),
122        "TerminologyCapabilities",
123        TIMEOUT_NORMAL).getReference();
124    } catch (Exception e) {
125      throw new FHIRException("Error fetching the server's terminology capabilities", e);
126    }
127    return capabilities;
128  }
129
130  public CapabilityStatement getCapabilitiesStatement() {
131    CapabilityStatement conformance = null;
132    try {
133      conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
134        getPreferredResourceFormat(),
135        generateHeaders(),
136        "CapabilitiesStatement",
137        TIMEOUT_NORMAL).getReference();
138    } catch (Exception e) {
139      throw new FHIRException("Error fetching the server's conformance statement", e);
140    }
141    return conformance;
142  }
143
144  public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException {
145    if (capabilities != null) return capabilities;
146    try {
147      capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
148        getPreferredResourceFormat(),
149        generateHeaders(),
150        "CapabilitiesStatement-Quick",
151        TIMEOUT_NORMAL).getReference();
152    } catch (Exception e) {
153      throw new FHIRException("Error fetching the server's capability statement: "+e.getMessage(), e);
154    }
155    return capabilities;
156  }
157
158  public <T extends Resource> T read(Class<T> resourceClass, String id) {//TODO Change this to AddressableResource
159    ResourceRequest<T> result = null;
160    try {
161      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
162        getPreferredResourceFormat(),
163        generateHeaders(),
164        "Read " + resourceClass.getName() + "/" + id,
165        TIMEOUT_NORMAL);
166      if (result.isUnsuccessfulRequest()) {
167        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
168      }
169    } catch (Exception e) {
170      throw new FHIRException(e);
171    }
172    return result.getPayload();
173  }
174
175  public <T extends Resource> T vread(Class<T> resourceClass, String id, String version) {
176    ResourceRequest<T> result = null;
177    try {
178      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
179        getPreferredResourceFormat(),
180        generateHeaders(),
181        "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
182        TIMEOUT_NORMAL);
183      if (result.isUnsuccessfulRequest()) {
184        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
185      }
186    } catch (Exception e) {
187      throw new FHIRException("Error trying to read this version of the resource", e);
188    }
189    return result.getPayload();
190  }
191
192  public <T extends Resource> T getCanonical(Class<T> resourceClass, String canonicalURL) {
193    ResourceRequest<T> result = null;
194    try {
195      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
196        getPreferredResourceFormat(),
197        generateHeaders(),
198        "Read " + resourceClass.getName() + "?url=" + canonicalURL,
199        TIMEOUT_NORMAL);
200      if (result.isUnsuccessfulRequest()) {
201        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
202      }
203    } catch (Exception e) {
204      handleException("An error has occurred while trying to read this version of the resource", e);
205    }
206    Bundle bnd = (Bundle) result.getPayload();
207    if (bnd.getEntry().size() == 0)
208      throw new EFhirClientException("No matching resource found for canonical URL '" + canonicalURL + "'");
209    if (bnd.getEntry().size() > 1)
210      throw new EFhirClientException("Multiple matching resources found for canonical URL '" + canonicalURL + "'");
211    return (T) bnd.getEntry().get(0).getResource();
212  }
213
214  public Resource update(Resource resource) {
215    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
216    try {
217      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
218        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
219        getPreferredResourceFormat(),
220        generateHeaders(),
221        "Update " + resource.fhirType() + "/" + resource.getId(),
222        TIMEOUT_OPERATION);
223      if (result.isUnsuccessfulRequest()) {
224        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
225      }
226    } catch (Exception e) {
227      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
228    }
229    // TODO oe 26.1.2015 could be made nicer if only OperationOutcome locationheader is returned with an operationOutcome would be returned (and not  the resource also) we make another read
230    try {
231      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
232      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
233      return this.vread(resource.getClass(), resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
234    } catch (ClassCastException e) {
235      // if we fall throught we have the correct type already in the create
236    }
237
238    return result.getPayload();
239  }
240
241  public <T extends Resource> T update(Class<T> resourceClass, T resource, String id) {
242    ResourceRequest<T> result = null;
243    try {
244      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
245        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
246        getPreferredResourceFormat(),
247        generateHeaders(),
248        "Update " + resource.fhirType() + "/" + id,
249        TIMEOUT_OPERATION);
250      if (result.isUnsuccessfulRequest()) {
251        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
252      }
253    } catch (Exception e) {
254      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
255    }
256    // TODO oe 26.1.2015 could be made nicer if only OperationOutcome   locationheader is returned with an operationOutcome would be returned (and not  the resource also) we make another read
257    try {
258      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
259      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
260      return this.vread(resourceClass, resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
261    } catch (ClassCastException e) {
262      // if we fall through we have the correct type already in the create
263    }
264
265    return result.getPayload();
266  }
267
268  public <T extends Resource> Parameters operateType(Class<T> resourceClass, String name, Parameters params) {
269    boolean complex = false;
270    for (ParametersParameterComponent p : params.getParameter())
271      complex = complex || !(p.getValue() instanceof PrimitiveType);
272    String ps = "";
273    try {
274      if (!complex)
275      for (ParametersParameterComponent p : params.getParameter())
276        if (p.getValue() instanceof PrimitiveType)
277          ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue()) + "&";
278    ResourceRequest<T> result;
279    URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
280    if (complex) {
281      byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()));
282      if (client.getLogger() != null) {
283        client.getLogger().logRequest("POST", url.toString(), null, body);
284      }
285      result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(),
286          "POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
287    } else {
288      if (client.getLogger() != null) {
289        client.getLogger().logRequest("GET", url.toString(), null, null);
290      }
291      result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
292    }
293    if (result.isUnsuccessfulRequest()) {
294      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
295    }
296    if (result.getPayload() instanceof Parameters) {
297      return (Parameters) result.getPayload();
298    } else {
299      Parameters p_out = new Parameters();
300      p_out.addParameter().setName("return").setResource(result.getPayload());
301      return p_out;
302    }
303    } catch (Exception e) {
304          handleException("Error performing tx3 operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);              
305    }
306    return null;
307  }
308
309
310  public Bundle transaction(Bundle batch) {
311    Bundle transactionResult = null;
312    try {
313      transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), "transaction", TIMEOUT_OPERATION + (TIMEOUT_ENTRY * batch.getEntry().size()));
314    } catch (Exception e) {
315      handleException("An error occurred trying to process this transaction request", e);
316    }
317    return transactionResult;
318  }
319
320  @SuppressWarnings("unchecked")
321  public <T extends Resource> OperationOutcome validate(Class<T> resourceClass, T resource, String id) {
322    ResourceRequest<T> result = null;
323    try {
324      result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
325        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
326        getPreferredResourceFormat(), generateHeaders(),
327        "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", TIMEOUT_OPERATION_LONG);
328      if (result.isUnsuccessfulRequest()) {
329        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
330      }
331    } catch (Exception e) {
332      handleException("An error has occurred while trying to validate this resource", e);
333    }
334    return (OperationOutcome) result.getPayload();
335  }
336
337  /**
338   * Helper method to prevent nesting of previously thrown EFhirClientExceptions
339   *
340   * @param e
341   * @throws EFhirClientException
342   */
343  protected void handleException(String message, Exception e) throws EFhirClientException {
344    if (e instanceof EFhirClientException) {
345      throw (EFhirClientException) e;
346    } else {
347      throw new EFhirClientException(message, e);
348    }
349  }
350
351  /**
352   * Helper method to determine whether desired resource representation
353   * is Json or XML.
354   *
355   * @param format
356   * @return
357   */
358  protected boolean isJson(String format) {
359    boolean isJson = false;
360    if (format.toLowerCase().contains("json")) {
361      isJson = true;
362    }
363    return isJson;
364  }
365
366  public Bundle fetchFeed(String url) {
367    Bundle feed = null;
368    try {
369      feed = client.issueGetFeedRequest(new URI(url), getPreferredResourceFormat());
370    } catch (Exception e) {
371      handleException("An error has occurred while trying to retrieve history since last update", e);
372    }
373    return feed;
374  }
375
376  public ValueSet expandValueset(ValueSet source, Parameters expParams) {
377    Parameters p = expParams == null ? new Parameters() : expParams.copy();
378    p.addParameter().setName("valueSet").setResource(source);
379    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
380    try {
381      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
382        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
383        getPreferredResourceFormat(),
384        generateHeaders(),
385        "ValueSet/$expand?url=" + source.getUrl(),
386        TIMEOUT_OPERATION_EXPAND);
387      if (result.isUnsuccessfulRequest()) {
388        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
389      }
390    } catch (IOException e) {
391      e.printStackTrace();
392    }
393    return result == null ? null : (ValueSet) result.getPayload();
394  }
395
396
397  public Parameters lookupCode(Map<String, String> params) {
398    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
399    try {
400      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
401        getPreferredResourceFormat(),
402        generateHeaders(),
403        "CodeSystem/$lookup",
404        TIMEOUT_NORMAL);
405    } catch (IOException e) {
406      e.printStackTrace();
407    }
408    if (result.isUnsuccessfulRequest()) {
409      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
410    }
411    return (Parameters) result.getPayload();
412  }
413
414  public ValueSet expandValueset(ValueSet source, ExpansionProfile profile, Map<String, String> params) {
415    Parameters p = new Parameters();
416    p.addParameter().setName("valueSet").setResource(source);
417    if (profile != null)
418      p.addParameter().setName("profile").setResource(profile);
419
420    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
421    try {
422      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
423        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
424        getPreferredResourceFormat(),
425        generateHeaders(),
426        "ValueSet/$expand?url=" + source.getUrl(),
427        TIMEOUT_OPERATION_EXPAND);
428      if (result.isUnsuccessfulRequest()) {
429        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
430      }
431    } catch (IOException e) {
432      e.printStackTrace();
433    }
434    return result == null ? null : (ValueSet) result.getPayload();
435  }
436
437  public ValueSet expandValueset(ValueSet source, Parameters expParams, Map<String, String> params) {
438    Parameters p = expParams == null ? new Parameters() : expParams.copy();
439    p.addParameter().setName("valueSet").setResource(source);
440    for (String n : params.keySet()) {
441      p.addParameter().setName(n).setValue(new StringType(params.get(n)));
442    }
443    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
444    try {
445
446      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
447        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
448        getPreferredResourceFormat(),
449        generateHeaders(),
450        "ValueSet/$expand?url=" + source.getUrl(),
451        TIMEOUT_OPERATION_EXPAND);
452      if (result.isUnsuccessfulRequest()) {
453        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
454      }
455    } catch (IOException e) {
456      e.printStackTrace();
457    }
458    return result == null ? null : (ValueSet) result.getPayload();
459  }
460
461  public String getAddress() {
462    return base;
463  }
464
465  public ConceptMap initializeClosure(String name) {
466    Parameters params = new Parameters();
467    params.addParameter().setName("name").setValue(new StringType(name));
468    ResourceRequest<Resource> result = null;
469    try {
470      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
471        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
472        getPreferredResourceFormat(),
473        generateHeaders(),
474        "Closure?name=" + name,
475        TIMEOUT_NORMAL);
476      if (result.isUnsuccessfulRequest()) {
477        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
478      }
479    } catch (IOException e) {
480      e.printStackTrace();
481    }
482    return result == null ? null : (ConceptMap) result.getPayload();
483  }
484
485  public ConceptMap updateClosure(String name, Coding coding) {
486    Parameters params = new Parameters();
487    params.addParameter().setName("name").setValue(new StringType(name));
488    params.addParameter().setName("concept").setValue(coding);
489    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
490    try {
491      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
492        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
493        getPreferredResourceFormat(),
494        generateHeaders(),
495        "UpdateClosure?name=" + name,
496        TIMEOUT_OPERATION);
497      if (result.isUnsuccessfulRequest()) {
498        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
499      }
500    } catch (IOException e) {
501      e.printStackTrace();
502    }
503    return result == null ? null : (ConceptMap) result.getPayload();
504  }
505
506  public String getUsername() {
507    return username;
508  }
509
510  public void setUsername(String username) {
511    this.username = username;
512  }
513
514  public String getPassword() {
515    return password;
516  }
517
518  public void setPassword(String password) {
519    this.password = password;
520  }
521
522  public long getTimeout() {
523    return client.getTimeout();
524  }
525
526  public void setTimeout(long timeout) {
527    client.setTimeout(timeout);
528  }
529
530  public ToolingClientLogger getLogger() {
531    return client.getLogger();
532  }
533
534  public void setLogger(ToolingClientLogger logger) {
535    client.setLogger(logger);
536  }
537
538  public int getRetryCount() {
539    return client.getRetryCount();
540  }
541
542  public void setRetryCount(int retryCount) {
543    client.setRetryCount(retryCount);
544  }
545
546  public void setClientHeaders(ArrayList<Header> headers) {
547    this.headers = headers;
548  }
549
550  private Headers generateHeaders() {
551    Headers.Builder builder = new Headers.Builder();
552    // Add basic auth header if it exists
553    if (basicAuthHeaderExists()) {
554      builder.add(getAuthorizationHeader().toString());
555    }
556    // Add any other headers
557    if(this.headers != null) {
558      this.headers.forEach(header -> builder.add(header.toString()));
559    }
560    if (!Utilities.noString(userAgent)) {
561      builder.add("User-Agent: "+userAgent);
562    }
563    return builder.build();
564  }
565
566  public boolean basicAuthHeaderExists() {
567    return (username != null) && (password != null);
568  }
569
570  public Header getAuthorizationHeader() {
571    String usernamePassword = username + ":" + password;
572    String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
573    return new Header("Authorization", "Basic " + base64usernamePassword);
574  }
575  
576  public String getUserAgent() {
577    return userAgent;
578  }
579
580  public void setUserAgent(String userAgent) {
581    this.userAgent = userAgent;
582  }
583
584  public String getServerVersion() {
585    return capabilities == null ? null : capabilities.getSoftware().getVersion();
586  }
587}
588