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