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.FhirPublication;
030import org.hl7.fhir.utilities.ToolingClientLogger;
031import org.hl7.fhir.utilities.Utilities;
032
033import okhttp3.Headers;
034import okhttp3.internal.http2.Header;
035
036/**
037 * Very Simple RESTful client. This is purely for use in the standalone
038 * tools jar packages. It doesn't support many features, only what the tools
039 * need.
040 * <p>
041 * To use, initialize class and set base service URI as follows:
042 *
043 * <pre><code>
044 * FHIRSimpleClient fhirClient = new FHIRSimpleClient();
045 * fhirClient.initialize("http://my.fhir.domain/myServiceRoot");
046 * </code></pre>
047 * <p>
048 * Default Accept and Content-Type headers are application/fhir+xml and application/fhir+json.
049 * <p>
050 * These can be changed by invoking the following setter functions:
051 *
052 * <pre><code>
053 * setPreferredResourceFormat()
054 * setPreferredFeedFormat()
055 * </code></pre>
056 * <p>
057 * TODO Review all sad paths.
058 *
059 * @author Claude Nanjo
060 */
061public class FHIRToolingClient {
062
063  public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssK";
064  public static final String DATE_FORMAT = "yyyy-MM-dd";
065  public static final String hostKey = "http.proxyHost";
066  public static final String portKey = "http.proxyPort";
067
068  private static final int TIMEOUT_NORMAL = 1500;
069  private static final int TIMEOUT_OPERATION = 30000;
070  private static final int TIMEOUT_ENTRY = 500;
071  private static final int TIMEOUT_OPERATION_LONG = 60000;
072  private static final int TIMEOUT_OPERATION_EXPAND = 120000;
073
074  private String base;
075  private ResourceAddress resourceAddress;
076  private ResourceFormat preferredResourceFormat;
077  private int maxResultSetSize = -1;//_count
078  private CapabilityStatement capabilities;
079  private Client client = new Client();
080  private ArrayList<Header> headers = new ArrayList<>();
081  private String username;
082  private String password;
083  private String userAgent;
084  private EnumSet<FhirPublication> allowedVersions;
085  private String acceptLang;
086
087  //Pass endpoint for client - URI
088  public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
089    preferredResourceFormat = ResourceFormat.RESOURCE_XML;
090    this.userAgent = userAgent;
091    this.allowedVersions = supportableVersions();
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.allowedVersions = supportableVersions();
100    this.maxResultSetSize = -1;
101  }
102
103  public Client getClient() {
104    return client;
105  }
106
107  public EnumSet<FhirPublication> supportableVersions() {
108    // todo
109    return EnumSet.range(FhirPublication.STU3, FhirPublication.R5);
110  }
111  
112  public void setAllowedVersions(EnumSet<FhirPublication> versions) {
113    // todo
114  }
115  
116  public EnumSet<FhirPublication> getAllowedVersions() {
117    return null; // todo
118  }
119  
120  public FhirPublication getActualVersion() {
121    return FhirPublication.STU3;
122  }
123  
124  public void setClient(Client client) {
125    this.client = client;
126  }
127
128  private void checkCapabilities() {
129    try {
130      capabilities = getCapabilitiesStatementQuick();
131    } catch (Throwable e) {
132    }
133  }
134
135  public String getPreferredResourceFormat() {
136    return preferredResourceFormat.getHeader();
137  }
138
139  public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
140    preferredResourceFormat = resourceFormat;
141  }
142
143  public int getMaximumRecordCount() {
144    return maxResultSetSize;
145  }
146
147  public void setMaximumRecordCount(int maxResultSetSize) {
148    this.maxResultSetSize = maxResultSetSize;
149  }
150
151  public Parameters getTerminologyCapabilities() {
152    Parameters capabilities = null;
153    try {
154      capabilities = (Parameters) client.issueGetResourceRequest(resourceAddress.resolveMetadataTxCaps(),
155        getPreferredResourceFormat(),
156        generateHeaders(),
157        "TerminologyCapabilities",
158        TIMEOUT_NORMAL).getReference();
159    } catch (Exception e) {
160      throw new FHIRException("Error fetching the server's terminology capabilities", e);
161    }
162    return capabilities;
163  }
164
165  public CapabilityStatement getCapabilitiesStatement() {
166    CapabilityStatement conformance = null;
167    try {
168      conformance = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(false),
169        getPreferredResourceFormat(),
170        generateHeaders(),
171        "CapabilitiesStatement",
172        TIMEOUT_NORMAL).getReference();
173    } catch (Exception e) {
174      throw new FHIRException("Error fetching the server's conformance statement", e);
175    }
176    return conformance;
177  }
178
179  public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException {
180    if (capabilities != null) return capabilities;
181    try {
182      capabilities = (CapabilityStatement) client.issueGetResourceRequest(resourceAddress.resolveMetadataUri(true),
183        getPreferredResourceFormat(),
184        generateHeaders(),
185        "CapabilitiesStatement-Quick",
186        TIMEOUT_NORMAL).getReference();
187    } catch (Exception e) {
188      throw new FHIRException("Error fetching the server's capability statement: "+e.getMessage(), e);
189    }
190    return capabilities;
191  }
192
193  public <T extends Resource> T read(Class<T> resourceClass, String id) {//TODO Change this to AddressableResource
194    ResourceRequest<T> result = null;
195    try {
196      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
197        getPreferredResourceFormat(),
198        generateHeaders(),
199        "Read " + resourceClass.getName() + "/" + id,
200        TIMEOUT_NORMAL);
201      if (result.isUnsuccessfulRequest()) {
202        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
203      }
204    } catch (Exception e) {
205      throw new FHIRException(e);
206    }
207    return result.getPayload();
208  }
209
210  public <T extends Resource> T vread(Class<T> resourceClass, String id, String version) {
211    ResourceRequest<T> result = null;
212    try {
213      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
214        getPreferredResourceFormat(),
215        generateHeaders(),
216        "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
217        TIMEOUT_NORMAL);
218      if (result.isUnsuccessfulRequest()) {
219        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
220      }
221    } catch (Exception e) {
222      throw new FHIRException("Error trying to read this version of the resource", e);
223    }
224    return result.getPayload();
225  }
226
227  public <T extends Resource> T getCanonical(Class<T> resourceClass, String canonicalURL) {
228    ResourceRequest<T> result = null;
229    try {
230      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
231        getPreferredResourceFormat(),
232        generateHeaders(),
233        "Read " + resourceClass.getName() + "?url=" + canonicalURL,
234        TIMEOUT_NORMAL);
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    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
251    try {
252      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
253        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
254        getPreferredResourceFormat(),
255        generateHeaders(),
256        "Update " + resource.fhirType() + "/" + resource.getId(),
257        TIMEOUT_OPERATION);
258      if (result.isUnsuccessfulRequest()) {
259        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
260      }
261    } catch (Exception e) {
262      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
263    }
264    // 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
265    try {
266      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
267      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
268      return this.vread(resource.getClass(), resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
269    } catch (ClassCastException e) {
270      // if we fall throught we have the correct type already in the create
271    }
272
273    return result.getPayload();
274  }
275
276  public <T extends Resource> T update(Class<T> resourceClass, T resource, String id) {
277    ResourceRequest<T> result = null;
278    try {
279      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
280        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
281        getPreferredResourceFormat(),
282        generateHeaders(),
283        "Update " + resource.fhirType() + "/" + id,
284        TIMEOUT_OPERATION);
285      if (result.isUnsuccessfulRequest()) {
286        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
287      }
288    } catch (Exception e) {
289      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
290    }
291    // 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
292    try {
293      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
294      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
295      return this.vread(resourceClass, resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
296    } catch (ClassCastException e) {
297      // if we fall through we have the correct type already in the create
298    }
299
300    return result.getPayload();
301  }
302
303  public <T extends Resource> Parameters operateType(Class<T> resourceClass, String name, Parameters params) {
304    boolean complex = false;
305    for (ParametersParameterComponent p : params.getParameter())
306      complex = complex || !(p.getValue() instanceof PrimitiveType);
307    String ps = "";
308    try {
309      if (!complex)
310      for (ParametersParameterComponent p : params.getParameter())
311        if (p.getValue() instanceof PrimitiveType)
312          ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue()) + "&";
313    ResourceRequest<T> result;
314    URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
315    if (complex) {
316      byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()));
317      if (client.getLogger() != null) {
318        client.getLogger().logRequest("POST", url.toString(), null, body);
319      }
320      result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(),
321          "POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
322    } else {
323      if (client.getLogger() != null) {
324        client.getLogger().logRequest("GET", url.toString(), null, null);
325      }
326      result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
327    }
328    if (result.isUnsuccessfulRequest()) {
329      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
330    }
331    if (result.getPayload() instanceof Parameters) {
332      return (Parameters) result.getPayload();
333    } else {
334      Parameters p_out = new Parameters();
335      p_out.addParameter().setName("return").setResource(result.getPayload());
336      return p_out;
337    }
338    } catch (Exception e) {
339          handleException("Error performing tx3 operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);              
340    }
341    return null;
342  }
343
344
345  public Bundle transaction(Bundle batch) {
346    Bundle transactionResult = null;
347    try {
348      transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(), "transaction", TIMEOUT_OPERATION + (TIMEOUT_ENTRY * batch.getEntry().size()));
349    } catch (Exception e) {
350      handleException("An error occurred trying to process this transaction request", e);
351    }
352    return transactionResult;
353  }
354
355  @SuppressWarnings("unchecked")
356  public <T extends Resource> OperationOutcome validate(Class<T> resourceClass, T resource, String id) {
357    ResourceRequest<T> result = null;
358    try {
359      result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
360        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
361        getPreferredResourceFormat(), generateHeaders(),
362        "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", TIMEOUT_OPERATION_LONG);
363      if (result.isUnsuccessfulRequest()) {
364        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
365      }
366    } catch (Exception e) {
367      handleException("An error has occurred while trying to validate this resource", e);
368    }
369    return (OperationOutcome) result.getPayload();
370  }
371
372  /**
373   * Helper method to prevent nesting of previously thrown EFhirClientExceptions
374   *
375   * @param e
376   * @throws EFhirClientException
377   */
378  protected void handleException(String message, Exception e) throws EFhirClientException {
379    if (e instanceof EFhirClientException) {
380      throw (EFhirClientException) e;
381    } else {
382      throw new EFhirClientException(message, e);
383    }
384  }
385
386  /**
387   * Helper method to determine whether desired resource representation
388   * is Json or XML.
389   *
390   * @param format
391   * @return
392   */
393  protected boolean isJson(String format) {
394    boolean isJson = false;
395    if (format.toLowerCase().contains("json")) {
396      isJson = true;
397    }
398    return isJson;
399  }
400
401  public Bundle fetchFeed(String url) {
402    Bundle feed = null;
403    try {
404      feed = client.issueGetFeedRequest(new URI(url), getPreferredResourceFormat());
405    } catch (Exception e) {
406      handleException("An error has occurred while trying to retrieve history since last update", e);
407    }
408    return feed;
409  }
410
411  public ValueSet expandValueset(ValueSet source, Parameters expParams) {
412    Parameters p = expParams == null ? new Parameters() : expParams.copy();
413    p.addParameter().setName("valueSet").setResource(source);
414    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
415    try {
416      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
417        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
418        getPreferredResourceFormat(),
419        generateHeaders(),
420        "ValueSet/$expand?url=" + source.getUrl(),
421        TIMEOUT_OPERATION_EXPAND);
422      if (result.isUnsuccessfulRequest()) {
423        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
424      }
425    } catch (IOException e) {
426      e.printStackTrace();
427    }
428    return result == null ? null : (ValueSet) result.getPayload();
429  }
430
431
432  public Parameters lookupCode(Map<String, String> params) {
433    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
434    try {
435      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
436        getPreferredResourceFormat(),
437        generateHeaders(),
438        "CodeSystem/$lookup",
439        TIMEOUT_NORMAL);
440    } catch (IOException e) {
441      e.printStackTrace();
442    }
443    if (result.isUnsuccessfulRequest()) {
444      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
445    }
446    return (Parameters) result.getPayload();
447  }
448
449  public ValueSet expandValueset(ValueSet source, ExpansionProfile profile, Map<String, String> params) {
450    Parameters p = new Parameters();
451    p.addParameter().setName("valueSet").setResource(source);
452    if (profile != null)
453      p.addParameter().setName("profile").setResource(profile);
454
455    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
456    try {
457      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
458        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
459        getPreferredResourceFormat(),
460        generateHeaders(),
461        "ValueSet/$expand?url=" + source.getUrl(),
462        TIMEOUT_OPERATION_EXPAND);
463      if (result.isUnsuccessfulRequest()) {
464        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
465      }
466    } catch (IOException e) {
467      e.printStackTrace();
468    }
469    return result == null ? null : (ValueSet) result.getPayload();
470  }
471
472  public ValueSet expandValueset(ValueSet source, Parameters expParams, Map<String, String> params) {
473    Parameters p = expParams == null ? new Parameters() : expParams.copy();
474    p.addParameter().setName("valueSet").setResource(source);
475    for (String n : params.keySet()) {
476      p.addParameter().setName(n).setValue(new StringType(params.get(n)));
477    }
478    org.hl7.fhir.dstu3.utils.client.network.ResourceRequest<Resource> result = null;
479    try {
480
481      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
482        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
483        getPreferredResourceFormat(),
484        generateHeaders(),
485        "ValueSet/$expand?url=" + source.getUrl(),
486        TIMEOUT_OPERATION_EXPAND);
487      if (result.isUnsuccessfulRequest()) {
488        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
489      }
490    } catch (IOException e) {
491      e.printStackTrace();
492    }
493    return result == null ? null : (ValueSet) result.getPayload();
494  }
495
496  public String getAddress() {
497    return base;
498  }
499
500  public ConceptMap initializeClosure(String name) {
501    Parameters params = new Parameters();
502    params.addParameter().setName("name").setValue(new StringType(name));
503    ResourceRequest<Resource> result = null;
504    try {
505      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
506        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
507        getPreferredResourceFormat(),
508        generateHeaders(),
509        "Closure?name=" + name,
510        TIMEOUT_NORMAL);
511      if (result.isUnsuccessfulRequest()) {
512        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
513      }
514    } catch (IOException e) {
515      e.printStackTrace();
516    }
517    return result == null ? null : (ConceptMap) result.getPayload();
518  }
519
520  public ConceptMap updateClosure(String name, Coding coding) {
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())),
528        getPreferredResourceFormat(),
529        generateHeaders(),
530        "UpdateClosure?name=" + name,
531        TIMEOUT_OPERATION);
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    return builder.build();
602  }
603
604  public boolean basicAuthHeaderExists() {
605    return (username != null) && (password != null);
606  }
607
608  public Header getAuthorizationHeader() {
609    String usernamePassword = username + ":" + password;
610    String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
611    return new Header("Authorization", "Basic " + base64usernamePassword);
612  }
613  
614  public String getUserAgent() {
615    return userAgent;
616  }
617
618  public void setUserAgent(String userAgent) {
619    this.userAgent = userAgent;
620  }
621
622  public String getServerVersion() {
623    checkCapabilities();
624    return capabilities == null ? null : capabilities.getSoftware().getVersion();
625  }
626
627  public void setLanguage(String lang) {
628    this.acceptLang = lang;
629  }
630}
631