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 += p.getName() + "=" + Utilities.encodeUriParam(((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  /**
348   * Helper method to prevent nesting of previously thrown EFhirClientExceptions. If the e param is an instance of
349   * EFhirClientException, it will be rethrown. Otherwise, a new EFhirClientException will be thrown with e as the
350   * cause.
351   *
352   * @param code The EFhirClientException code.
353   * @param message The EFhirClientException message.
354   * @param e The exception.
355   * @throws EFhirClientException representing the exception.
356   */
357  protected void handleException(int code, String message, Exception e) throws EFhirClientException {
358    if (e instanceof EFhirClientException) {
359      throw (EFhirClientException) e;
360    } else {
361      throw new EFhirClientException(code, message, e);
362    }
363  }
364
365  /**
366   * Helper method to determine whether desired resource representation
367   * is Json or XML.
368   *
369   * @param format The format
370   * @return true if the format is JSON, false otherwise
371   */
372  protected boolean isJson(String format) {
373    boolean isJson = false;
374    if (format.toLowerCase().contains("json")) {
375      isJson = true;
376    }
377    return isJson;
378  }
379
380  public Bundle fetchFeed(String url) {
381    recordUse();
382    Bundle feed = null;
383    try {
384      feed = client.issueGetFeedRequest(new URI(url), withVer(getPreferredResourceFormat(), "4.0"), timeoutLong);
385    } catch (Exception e) {
386      handleException(0, "An error has occurred while trying to read a bundle", e);
387    }
388    return feed;
389  }
390
391  public Parameters lookupCode(Map<String, String> params) {
392    recordUse();
393    org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
394    try {
395      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
396          withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(false), "CodeSystem/$lookup", timeoutNormal);
397    } catch (IOException e) {
398      throw new FHIRException(e);
399    }
400    if (result.isUnsuccessfulRequest()) {
401      throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
402          (OperationOutcome) result.getPayload());
403    }
404    return (Parameters) result.getPayload();
405  }
406
407  public Parameters lookupCode(Parameters p) {
408    recordUse();
409    org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
410    try {
411      result = client.issuePostRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup"),
412          ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
413          withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "CodeSystem/$lookup", timeoutNormal);
414    } catch (IOException e) {
415      throw new FHIRException(e);
416    }
417    if (result.isUnsuccessfulRequest()) {
418      throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
419          (OperationOutcome) result.getPayload());
420    }
421    return (Parameters) result.getPayload();
422  }
423
424  public Parameters translate(Parameters p) {
425    recordUse();
426    org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
427    try {
428      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ConceptMap.class, "translate"),
429          ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
430          withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "ConceptMap/$translate", timeoutNormal);
431    } catch (IOException e) {
432      throw new FHIRException(e);
433    }
434    if (result.isUnsuccessfulRequest()) {
435      throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
436          (OperationOutcome) result.getPayload());
437    }
438    return (Parameters) result.getPayload();
439  }
440  
441  public ValueSet expandValueset(ValueSet source, Parameters expParams) {
442    recordUse();
443    Parameters p = expParams == null ? new Parameters() : expParams.copy();
444    if (source != null) {
445      p.addParameter().setName("valueSet").setResource(source);
446    }
447    org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
448    try {
449      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
450          ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true), withVer(getPreferredResourceFormat(), "4.0"),
451          generateHeaders(true), source == null ? "ValueSet/$expand" : "ValueSet/$expand?url=" + source.getUrl(),
452              timeoutExpand);
453      if (result.isUnsuccessfulRequest()) {
454        throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
455            (OperationOutcome) result.getPayload());
456      }
457    } catch (EFhirClientException e) {
458      if (e.getServerErrors().size() > 0) {
459        throw new EFhirClientException(e.getCode(), e.getMessage(), e.getServerErrors().get(0));
460      } else {
461        throw new EFhirClientException(e.getCode(), e.getMessage(), e);
462      }
463    } catch (Exception e) {
464      throw new EFhirClientException(0, e.getMessage(), e);
465    }
466    return result == null ? null : (ValueSet) result.getPayload();
467  }
468
469  public String getAddress() {
470    return base;
471  }
472
473  public ConceptMap initializeClosure(String name) {
474    recordUse();
475    Parameters params = new Parameters();
476    params.addParameter().setName("name").setValue(new StringType(name));
477    ResourceRequest<Resource> result = null;
478    try {
479      result = client.issuePostRequest(
480          resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
481          ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
482          withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "Closure?name=" + name, timeoutNormal);
483      if (result.isUnsuccessfulRequest()) {
484        throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
485            (OperationOutcome) result.getPayload());
486      }
487    } catch (IOException e) {
488      throw new FHIRException(e);
489    }
490    return result == null ? null : (ConceptMap) result.getPayload();
491  }
492
493  public ConceptMap updateClosure(String name, Coding coding) {
494    recordUse();
495    Parameters params = new Parameters();
496    params.addParameter().setName("name").setValue(new StringType(name));
497    params.addParameter().setName("concept").setValue(coding);
498    org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
499    try {
500      result = client.issuePostRequest(
501          resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
502          ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
503          withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), "UpdateClosure?name=" + name, timeoutOperation);
504      if (result.isUnsuccessfulRequest()) {
505        throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
506            (OperationOutcome) result.getPayload());
507      }
508    } catch (IOException e) {
509      throw new FHIRException(e);
510    }
511    return result == null ? null : (ConceptMap) result.getPayload();
512  }
513
514  public ToolingClientLogger getLogger() {
515    return client.getLogger();
516  }
517
518  public void setLogger(ToolingClientLogger logger) {
519    client.setLogger(logger);
520  }
521
522  public int getRetryCount() {
523    return client.getRetryCount();
524  }
525
526  public void setRetryCount(int retryCount) {
527    client.setRetryCount(retryCount);
528  }
529
530  public void setClientHeaders(Iterable<HTTPHeader> headers) {
531    this.headers = new ArrayList<>();
532    headers.forEach(this.headers::add);
533  }
534
535  private Iterable<HTTPHeader> generateHeaders(boolean hasBody) {
536    // Add any other headers
537    List<HTTPHeader> headers = new ArrayList<>(this.headers);
538    if (!Utilities.noString(userAgent)) {
539      headers.add(new HTTPHeader("User-Agent",userAgent));
540    }
541
542    if (!Utilities.noString(acceptLanguage)) {
543      headers.add(new HTTPHeader("Accept-Language", acceptLanguage));
544    }
545
546    if (hasBody && !Utilities.noString(contentLanguage)) {
547      headers.add(new HTTPHeader("Content-Language",contentLanguage));
548    }
549
550    return headers;
551  }
552
553  public String getServerVersion() {
554    return getCapabilitiesStatementQuick().getSoftware().getVersion();
555  }
556
557  public Bundle search(String type, String criteria) {
558    recordUse();
559    return fetchFeed(Utilities.pathURL(base, type+criteria));
560  }
561
562  public <T extends Resource> T fetchResource(Class<T> resourceClass, String id) {
563    recordUse();
564    org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
565    try {
566      result = client.issueGetResourceRequest(resourceAddress.resolveGetResource(resourceClass, id),
567          withVer(getPreferredResourceFormat(), "4.0"), generateHeaders(true), resourceClass.getName()+"/"+id, timeoutNormal);
568    } catch (IOException e) {
569      throw new FHIRException(e);
570    }
571    if (result.isUnsuccessfulRequest()) {
572      throw new EFhirClientException(result.getHttpStatus(), "Server returned error code " + result.getHttpStatus(),
573          (OperationOutcome) result.getPayload());
574    }
575    return (T) result.getPayload();
576  }
577
578  public int getUseCount() {
579    return useCount;
580  }
581
582  private void recordUse() {
583    useCount++;    
584  }
585
586  
587}