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