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