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