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