001package org.hl7.fhir.dstu3.utils.client;
002
003import java.io.IOException;
004import java.net.URI;
005import java.net.URISyntaxException;
006import java.util.ArrayList;
007import java.util.Base64;
008import java.util.EnumSet;
009import java.util.HashMap;
010import java.util.Map;
011
012import org.hl7.fhir.dstu3.model.Bundle;
013import org.hl7.fhir.dstu3.model.CapabilityStatement;
014import org.hl7.fhir.dstu3.model.CodeSystem;
015import org.hl7.fhir.dstu3.model.Coding;
016import org.hl7.fhir.dstu3.model.ConceptMap;
017import org.hl7.fhir.dstu3.model.ExpansionProfile;
018import org.hl7.fhir.dstu3.model.OperationOutcome;
019import org.hl7.fhir.dstu3.model.Parameters;
020import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
021import org.hl7.fhir.dstu3.model.PrimitiveType;
022import org.hl7.fhir.dstu3.model.Resource;
023import org.hl7.fhir.dstu3.model.StringType;
024import org.hl7.fhir.dstu3.model.ValueSet;
025import org.hl7.fhir.dstu3.utils.client.network.ByteUtils;
026import org.hl7.fhir.dstu3.utils.client.network.Client;
027import org.hl7.fhir.dstu3.utils.client.network.ResourceRequest;
028import org.hl7.fhir.exceptions.FHIRException;
029import org.hl7.fhir.utilities.FHIRBaseToolingClient;
030import org.hl7.fhir.utilities.FhirPublication;
031import org.hl7.fhir.utilities.ToolingClientLogger;
032import org.hl7.fhir.utilities.Utilities;
033
034import okhttp3.Headers;
035import okhttp3.internal.http2.Header;
036
037/**
038 * Very Simple RESTful client. This is purely for use in the standalone
039 * tools jar packages. It doesn't support many features, only what the tools
040 * need.
041 * <p>
042 * To use, initialize class and set base service URI as follows:
043 *
044 * <pre><code>
045 * FHIRSimpleClient fhirClient = new FHIRSimpleClient();
046 * fhirClient.initialize("http://my.fhir.domain/myServiceRoot");
047 * </code></pre>
048 * <p>
049 * Default Accept and Content-Type headers are application/fhir+xml and application/fhir+json.
050 * <p>
051 * These can be changed by invoking the following setter functions:
052 *
053 * <pre><code>
054 * setPreferredResourceFormat()
055 * setPreferredFeedFormat()
056 * </code></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  private ResourceFormat preferredResourceFormat;
072  private int maxResultSetSize = -1;//_count
073  private CapabilityStatement capabilities;
074  private Client client = new Client();
075  private ArrayList<Header> headers = new ArrayList<>();
076  private String username;
077  private String password;
078  private String userAgent;
079  private EnumSet<FhirPublication> allowedVersions;
080  private String acceptLang;
081  private String contentLang;
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(),
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(),
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(),
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(),
197        "Read " + resourceClass.getName() + "/" + id,
198        timeoutNormal);
199      if (result.isUnsuccessfulRequest()) {
200        throw new EFhirClientException("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(),
215        "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
216        timeoutNormal);
217      if (result.isUnsuccessfulRequest()) {
218        throw new EFhirClientException("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(),
233        "Read " + resourceClass.getName() + "?url=" + canonicalURL,
234        timeoutNormal);
235      if (result.isUnsuccessfulRequest()) {
236        throw new EFhirClientException("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(),
257        "Update " + resource.fhirType() + "/" + resource.getId(),
258        timeoutOperation);
259      if (result.isUnsuccessfulRequest()) {
260        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
261      }
262    } catch (Exception e) {
263      throw new EFhirClientException("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(),
285        "Update " + resource.fhirType() + "/" + id,
286        timeoutOperation);
287      if (result.isUnsuccessfulRequest()) {
288        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
289      }
290    } catch (Exception e) {
291      throw new EFhirClientException("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 += p.getName() + "=" + Utilities.encodeUri(((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(),
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(), "GET " + resourceClass.getName() + "/$" + name, timeoutLong);
330    }
331    if (result.isUnsuccessfulRequest()) {
332      throw new EFhirClientException("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(),
367        "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", timeoutLong);
368      if (result.isUnsuccessfulRequest()) {
369        throw new EFhirClientException("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  /**
378   * Helper method to prevent nesting of previously thrown EFhirClientExceptions
379   *
380   * @param e
381   * @throws EFhirClientException
382   */
383  protected void handleException(String message, Exception e) throws EFhirClientException {
384    if (e instanceof EFhirClientException) {
385      throw (EFhirClientException) e;
386    } else {
387      throw new EFhirClientException(message, e);
388    }
389  }
390
391  /**
392   * Helper method to determine whether desired resource representation
393   * is Json or XML.
394   *
395   * @param format
396   * @return
397   */
398  protected boolean isJson(String format) {
399    boolean isJson = false;
400    if (format.toLowerCase().contains("json")) {
401      isJson = true;
402    }
403    return isJson;
404  }
405
406  public Bundle fetchFeed(String url) {
407    recordUse();
408    Bundle feed = null;
409    try {
410      feed = client.issueGetFeedRequest(new URI(url), getPreferredResourceFormat());
411    } catch (Exception e) {
412      handleException("An error has occurred while trying to retrieve history since last update", e);
413    }
414    return feed;
415  }
416
417  public Parameters lookupCode(Map<String, String> params) {
418    recordUse();
419    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
420    try {
421      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
422        withVer(getPreferredResourceFormat(), "3.0"),
423        generateHeaders(),
424        "CodeSystem/$lookup",
425        timeoutNormal);
426    } catch (IOException e) {
427      e.printStackTrace();
428    }
429    if (result.isUnsuccessfulRequest()) {
430      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
431    }
432    return (Parameters) result.getPayload();
433  }
434
435  public Parameters lookupCode(Parameters p) {
436    recordUse();
437    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
438    try {
439      result = client.issuePostRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup"),
440          ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
441        withVer(getPreferredResourceFormat(), "3.0"),
442        generateHeaders(),
443        "CodeSystem/$lookup",
444        timeoutNormal);
445    } catch (IOException e) {
446      e.printStackTrace();
447    }
448    if (result.isUnsuccessfulRequest()) {
449      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
450    }
451    return (Parameters) result.getPayload();
452  }
453
454  public Parameters transform(Parameters p) {
455    recordUse();
456    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
457    try {
458      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ConceptMap.class, "transform"),
459          ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
460        withVer(getPreferredResourceFormat(), "3.0"),
461        generateHeaders(),
462        "ConceptMap/$transform",
463        timeoutNormal);
464    } catch (IOException e) {
465      e.printStackTrace();
466    }
467    if (result.isUnsuccessfulRequest()) {
468      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
469    }
470    return (Parameters) result.getPayload();
471  }
472
473  public ValueSet expandValueset(ValueSet source, Parameters expParams) {
474    recordUse();
475    Parameters p = expParams == null ? new Parameters() : expParams.copy();
476    p.addParameter().setName("valueSet").setResource(source);
477    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
478    try {
479      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
480        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat()), true),
481        withVer(getPreferredResourceFormat(), "3.0"),
482        generateHeaders(),
483        "ValueSet/$expand?url=" + source.getUrl(),
484        timeoutExpand);
485      if (result.isUnsuccessfulRequest()) {
486        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
487      }
488    } catch (IOException e) {
489      e.printStackTrace();
490    }
491    return result == null ? null : (ValueSet) result.getPayload();
492  }
493  
494  public String getAddress() {
495    return base;
496  }
497
498  public ConceptMap initializeClosure(String name) {
499    recordUse();
500    Parameters params = new Parameters();
501    params.addParameter().setName("name").setValue(new StringType(name));
502    ResourceRequest<Resource> result = null;
503    try {
504      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
505        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()), true),
506        withVer(getPreferredResourceFormat(), "3.0"),
507        generateHeaders(),
508        "Closure?name=" + name,
509        timeoutNormal);
510      if (result.isUnsuccessfulRequest()) {
511        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
512      }
513    } catch (IOException e) {
514      e.printStackTrace();
515    }
516    return result == null ? null : (ConceptMap) result.getPayload();
517  }
518
519  public ConceptMap updateClosure(String name, Coding coding) {
520    recordUse();
521    Parameters params = new Parameters();
522    params.addParameter().setName("name").setValue(new StringType(name));
523    params.addParameter().setName("concept").setValue(coding);
524    org.hl7.fhir.dstu3.utils.client.network.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(),
530        "UpdateClosure?name=" + name,
531        timeoutOperation);
532      if (result.isUnsuccessfulRequest()) {
533        throw new EFhirClientException("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 String getUsername() {
542    return username;
543  }
544
545  public void setUsername(String username) {
546    this.username = username;
547  }
548
549  public String getPassword() {
550    return password;
551  }
552
553  public void setPassword(String password) {
554    this.password = password;
555  }
556
557  public long getTimeout() {
558    return client.getTimeout();
559  }
560
561  public void setTimeout(long timeout) {
562    client.setTimeout(timeout);
563  }
564
565  public ToolingClientLogger getLogger() {
566    return client.getLogger();
567  }
568
569  public void setLogger(ToolingClientLogger logger) {
570    client.setLogger(logger);
571  }
572
573  public int getRetryCount() {
574    return client.getRetryCount();
575  }
576
577  public void setRetryCount(int retryCount) {
578    client.setRetryCount(retryCount);
579  }
580
581  public void setClientHeaders(ArrayList<Header> headers) {
582    this.headers = headers;
583  }
584
585  private Headers generateHeaders() {
586    Headers.Builder builder = new Headers.Builder();
587    // Add basic auth header if it exists
588    if (basicAuthHeaderExists()) {
589      builder.add(getAuthorizationHeader().toString());
590    }
591    // Add any other headers
592    if(this.headers != null) {
593      this.headers.forEach(header -> builder.add(header.toString()));
594    }
595    if (!Utilities.noString(userAgent)) {
596      builder.add("User-Agent: "+userAgent);
597    } 
598    if (!Utilities.noString(acceptLang)) {
599      builder.add("Accept-Language: "+acceptLang);
600    }
601    if (!Utilities.noString(contentLang)) {
602      builder.add("Content-Language: "+contentLang);
603    }
604    return builder.build();
605  }
606
607  public boolean basicAuthHeaderExists() {
608    return (username != null) && (password != null);
609  }
610
611  public Header getAuthorizationHeader() {
612    String usernamePassword = username + ":" + password;
613    String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
614    return new Header("Authorization", "Basic " + base64usernamePassword);
615  }
616  
617  public String getUserAgent() {
618    return userAgent;
619  }
620
621  public void setUserAgent(String userAgent) {
622    this.userAgent = userAgent;
623  }
624
625  public String getServerVersion() {
626    checkCapabilities();
627    return capabilities == null ? null : capabilities.getSoftware().getVersion();
628  }
629
630  public void setAcceptLanguage(String lang) {
631    this.acceptLang = lang;
632  }
633  public void setContentLanguage(String lang) {
634    this.contentLang = lang;
635  }
636
637  public int getUseCount() {
638    return useCount;
639  }
640
641  private void recordUse() {
642    useCount++;    
643  }
644
645  public Bundle search(String type, String criteria) {
646    recordUse();
647    return fetchFeed(Utilities.pathURL(base, type+criteria));
648  }
649  
650}
651