001package org.hl7.fhir.r5.utils.client;
002
003import okhttp3.Headers;
004import okhttp3.internal.http2.Header;
005import org.hl7.fhir.exceptions.FHIRException;
006
007/*
008  Copyright (c) 2011+, HL7, Inc.
009  All rights reserved.
010  
011  Redistribution and use in source and binary forms, with or without modification, 
012  are permitted provided that the following conditions are met:
013  
014   * Redistributions of source code must retain the above copyright notice, this 
015     list of conditions and the following disclaimer.
016   * Redistributions in binary form must reproduce the above copyright notice, 
017     this list of conditions and the following disclaimer in the documentation 
018     and/or other materials provided with the distribution.
019   * Neither the name of HL7 nor the names of its contributors may be used to 
020     endorse or promote products derived from this software without specific 
021     prior written permission.
022  
023  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
024  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
025  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
026  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
027  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
028  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
029  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
030  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
031  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
032  POSSIBILITY OF SUCH DAMAGE.
033  
034*/
035
036import org.hl7.fhir.r5.model.*;
037import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
038import org.hl7.fhir.r5.utils.client.network.ByteUtils;
039import org.hl7.fhir.r5.utils.client.network.Client;
040import org.hl7.fhir.r5.utils.client.network.ResourceRequest;
041import org.hl7.fhir.utilities.ToolingClientLogger;
042import org.hl7.fhir.utilities.Utilities;
043import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import java.io.IOException;
048import java.net.URI;
049import java.net.URISyntaxException;
050import java.util.*;
051import java.util.stream.Collectors;
052import java.util.stream.Stream;
053
054/**
055 * Very Simple RESTful client. This is purely for use in the standalone
056 * tools jar packages. It doesn't support many features, only what the tools
057 * need.
058 * <p>
059 * To use, initialize class and set base service URI as follows:
060 *
061 * <pre><code>
062 * FHIRSimpleClient fhirClient = new FHIRSimpleClient();
063 * fhirClient.initialize("http://my.fhir.domain/myServiceRoot");
064 * </code></pre>
065 * <p>
066 * Default Accept and Content-Type headers are application/fhir+xml and application/fhir+json.
067 * <p>
068 * These can be changed by invoking the following setter functions:
069 *
070 * <pre><code>
071 * setPreferredResourceFormat()
072 * setPreferredFeedFormat()
073 * </code></pre>
074 * <p>
075 * TODO Review all sad paths.
076 *
077 * @author Claude Nanjo
078 */
079public class FHIRToolingClient {
080
081  private static final Logger logger = LoggerFactory.getLogger(FHIRToolingClient.class);
082
083
084  public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssK";
085  public static final String DATE_FORMAT = "yyyy-MM-dd";
086  public static final String hostKey = "http.proxyHost";
087  public static final String portKey = "http.proxyPort";
088
089  private static final int TIMEOUT_NORMAL = 1500;
090  private static final int TIMEOUT_OPERATION = 30000;
091  private static final int TIMEOUT_ENTRY = 500;
092  private static final int TIMEOUT_OPERATION_LONG = 60000;
093  private static final int TIMEOUT_OPERATION_EXPAND = 120000;
094
095  private String base;
096  private ResourceAddress resourceAddress;
097  private ResourceFormat preferredResourceFormat;
098  private int maxResultSetSize = -1;//_count
099  private CapabilityStatement capabilities;
100  private Client client = new Client();
101  private ArrayList<Header> headers = new ArrayList<>();
102  private String username;
103  private String password;
104  private String userAgent;
105
106
107  private String acceptLang;
108
109  //Pass endpoint for client - URI
110  public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
111    preferredResourceFormat = ResourceFormat.RESOURCE_JSON;
112    this.userAgent = userAgent;
113    initialize(baseServiceUrl);
114  }
115
116  public void initialize(String baseServiceUrl) throws URISyntaxException {
117    base = baseServiceUrl;
118    client.setBase(base);
119    resourceAddress = new ResourceAddress(baseServiceUrl);
120    this.maxResultSetSize = -1;
121  }
122
123  public Client getClient() {
124    return client;
125  }
126
127  public void setClient(Client client) {
128    this.client = client;
129  }
130
131  public String getPreferredResourceFormat() {
132    return preferredResourceFormat.getHeader();
133  }
134
135  public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
136    preferredResourceFormat = resourceFormat;
137  }
138
139  public int getMaximumRecordCount() {
140    return maxResultSetSize;
141  }
142
143  public void setMaximumRecordCount(int maxResultSetSize) {
144    this.maxResultSetSize = maxResultSetSize;
145  }
146
147  private List<ResourceFormat> getResourceFormatsWithPreferredFirst() {
148    return Stream.concat(
149      Arrays.stream(new ResourceFormat[]{preferredResourceFormat}),
150      Arrays.stream(ResourceFormat.values()).filter(a -> a != preferredResourceFormat)
151    ).collect(Collectors.toList());
152  }
153
154  private <T extends Resource> T getCapabilities(URI resourceUri, String message, String exceptionMessage) throws FHIRException {
155    final List<ResourceFormat> resourceFormats = getResourceFormatsWithPreferredFirst();
156
157    for (ResourceFormat attemptedResourceFormat : resourceFormats) {
158      try {
159        T output =  (T) client.issueGetResourceRequest(resourceUri,
160          preferredResourceFormat.getHeader(),
161          generateHeaders(),
162          message,
163          TIMEOUT_NORMAL).getReference();
164        if (attemptedResourceFormat != preferredResourceFormat) {
165          setPreferredResourceFormat(attemptedResourceFormat);
166        }
167        return output;
168      } catch (Exception e) {
169        logger.warn("Failed attempt to fetch " + resourceUri, e);
170      }
171    }
172    throw new FHIRException(exceptionMessage);
173  }
174
175  public TerminologyCapabilities getTerminologyCapabilities() {
176    TerminologyCapabilities capabilities = null;
177
178    try {
179      capabilities = getCapabilities(resourceAddress.resolveMetadataTxCaps(),
180        "TerminologyCapabilities",
181        "Error fetching the server's terminology capabilities");
182    } catch (ClassCastException e) {
183      throw new FHIRException("Unexpected response format for Terminology Capability metadata", e);
184    }
185    return capabilities;
186  }
187
188  public CapabilityStatement getCapabilitiesStatement() {
189    CapabilityStatement capabilityStatement = null;
190
191      capabilityStatement = getCapabilities(resourceAddress.resolveMetadataUri(false),
192
193        "CapabilitiesStatement", "Error fetching the server's conformance statement");
194    return capabilityStatement;
195  }
196
197  public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException {
198    if (capabilities != null) return capabilities;
199
200       capabilities = getCapabilities(resourceAddress.resolveMetadataUri(true),
201
202        "CapabilitiesStatement-Quick",
203        "Error fetching the server's capability statement");
204
205    return capabilities;
206  }
207
208  public <T extends Resource> T read(Class<T> resourceClass, String id) {//TODO Change this to AddressableResource
209    ResourceRequest<T> result = null;
210    try {
211      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
212        getPreferredResourceFormat(),
213        generateHeaders(),
214        "Read " + resourceClass.getName() + "/" + id,
215        TIMEOUT_NORMAL);
216      if (result.isUnsuccessfulRequest()) {
217        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
218      }
219    } catch (Exception e) {
220      throw new FHIRException(e);
221    }
222    return result.getPayload();
223  }
224
225  public <T extends Resource> T vread(Class<T> resourceClass, String id, String version) {
226    ResourceRequest<T> result = null;
227    try {
228      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
229        getPreferredResourceFormat(),
230        generateHeaders(),
231        "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
232        TIMEOUT_NORMAL);
233      if (result.isUnsuccessfulRequest()) {
234        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
235      }
236    } catch (Exception e) {
237      throw new FHIRException("Error trying to read this version of the resource", e);
238    }
239    return result.getPayload();
240  }
241
242  public <T extends Resource> T getCanonical(Class<T> resourceClass, String canonicalURL) {
243    ResourceRequest<T> result = null;
244    try {
245      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
246        getPreferredResourceFormat(),
247        generateHeaders(),
248        "Read " + resourceClass.getName() + "?url=" + canonicalURL,
249        TIMEOUT_NORMAL);
250      if (result.isUnsuccessfulRequest()) {
251        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
252      }
253    } catch (Exception e) {
254      handleException("An error has occurred while trying to read this version of the resource", e);
255    }
256    Bundle bnd = (Bundle) result.getPayload();
257    if (bnd.getEntry().size() == 0)
258      throw new EFhirClientException("No matching resource found for canonical URL '" + canonicalURL + "'");
259    if (bnd.getEntry().size() > 1)
260      throw new EFhirClientException("Multiple matching resources found for canonical URL '" + canonicalURL + "'");
261    return (T) bnd.getEntry().get(0).getResource();
262  }
263
264  public Resource update(Resource resource) {
265    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
266    try {
267      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
268        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
269        getPreferredResourceFormat(),
270        generateHeaders(),
271        "Update " + resource.fhirType() + "/" + resource.getId(),
272        TIMEOUT_OPERATION);
273      if (result.isUnsuccessfulRequest()) {
274        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
275      }
276    } catch (Exception e) {
277      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
278    }
279    // 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
280    try {
281      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
282      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
283      return this.vread(resource.getClass(), resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
284    } catch (ClassCastException e) {
285      // if we fall throught we have the correct type already in the create
286    }
287
288    return result.getPayload();
289  }
290
291  public <T extends Resource> T update(Class<T> resourceClass, T resource, String id) {
292    ResourceRequest<T> result = null;
293    try {
294      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
295        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
296        getPreferredResourceFormat(),
297        generateHeaders(),
298        "Update " + resource.fhirType() + "/" + id,
299        TIMEOUT_OPERATION);
300      if (result.isUnsuccessfulRequest()) {
301        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
302      }
303    } catch (Exception e) {
304      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
305    }
306    // 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
307    try {
308      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
309      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
310      return this.vread(resourceClass, resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
311    } catch (ClassCastException e) {
312      // if we fall through we have the correct type already in the create
313    }
314
315    return result.getPayload();
316  }
317
318  public <T extends Resource> Parameters operateType(Class<T> resourceClass, String name, Parameters params) {
319    boolean complex = false;
320    for (ParametersParameterComponent p : params.getParameter())
321      complex = complex || !(p.getValue() instanceof PrimitiveType);
322    String ps = "";
323    try {
324      if (!complex)
325        for (ParametersParameterComponent p : params.getParameter())
326          if (p.getValue() instanceof PrimitiveType)
327            ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue()) + "&";
328      ResourceRequest<T> result;
329      URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
330      if (complex) {
331        byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()));
332        result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(),
333            "POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
334      } else {
335        result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
336      }
337      if (result.isUnsuccessfulRequest()) {
338        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
339      }
340      if (result.getPayload() instanceof Parameters) {
341        return (Parameters) result.getPayload();
342      } else {
343        Parameters p_out = new Parameters();
344        p_out.addParameter().setName("return").setResource(result.getPayload());
345        return p_out;
346      }
347    } catch (Exception e) {
348      handleException("Error performing tx5 operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);                  
349    }
350    return null;
351  }
352
353  public Bundle transaction(Bundle batch) {
354    Bundle transactionResult = null;
355    try {
356      transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(),
357          generateHeaders(),
358          "transaction", TIMEOUT_OPERATION + (TIMEOUT_ENTRY * batch.getEntry().size()));
359    } catch (Exception e) {
360      handleException("An error occurred trying to process this transaction request", e);
361    }
362    return transactionResult;
363  }
364
365  @SuppressWarnings("unchecked")
366  public <T extends Resource> OperationOutcome validate(Class<T> resourceClass, T resource, String id) {
367    ResourceRequest<T> result = null;
368    try {
369      result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
370        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
371        getPreferredResourceFormat(), generateHeaders(),
372        "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", TIMEOUT_OPERATION_LONG);
373      if (result.isUnsuccessfulRequest()) {
374        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
375      }
376    } catch (Exception e) {
377      handleException("An error has occurred while trying to validate this resource", e);
378    }
379    return (OperationOutcome) result.getPayload();
380  }
381
382  /**
383   * Helper method to prevent nesting of previously thrown EFhirClientExceptions
384   *
385   * @param e
386   * @throws EFhirClientException
387   */
388  protected void handleException(String message, Exception e) throws EFhirClientException {
389    if (e instanceof EFhirClientException) {
390      throw (EFhirClientException) e;
391    } else {
392      throw new EFhirClientException(message, e);
393    }
394  }
395
396  /**
397   * Helper method to determine whether desired resource representation
398   * is Json or XML.
399   *
400   * @param format
401   * @return
402   */
403  protected boolean isJson(String format) {
404    boolean isJson = false;
405    if (format.toLowerCase().contains("json")) {
406      isJson = true;
407    }
408    return isJson;
409  }
410
411  public Bundle fetchFeed(String url) {
412    Bundle feed = null;
413    try {
414      feed = client.issueGetFeedRequest(new URI(url), getPreferredResourceFormat());
415    } catch (Exception e) {
416      handleException("An error has occurred while trying to retrieve history since last update", e);
417    }
418    return feed;
419  }
420
421  public ValueSet expandValueset(ValueSet source, Parameters expParams) {
422    Parameters p = expParams == null ? new Parameters() : expParams.copy();
423    p.addParameter().setName("valueSet").setResource(source);
424    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
425    try {
426      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
427          ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
428          getPreferredResourceFormat(),
429          generateHeaders(),
430          "ValueSet/$expand?url=" + source.getUrl(),
431          TIMEOUT_OPERATION_EXPAND);
432    } catch (IOException e) {
433      throw new FHIRException(e);
434    }
435    if (result.isUnsuccessfulRequest()) {
436      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
437    }
438    return result == null ? null : (ValueSet) result.getPayload();
439  }
440
441
442  public Parameters lookupCode(Map<String, String> params) {
443    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
444    try {
445      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
446        getPreferredResourceFormat(),
447        generateHeaders(),
448        "CodeSystem/$lookup",
449        TIMEOUT_NORMAL);
450    } catch (IOException e) {
451      e.printStackTrace();
452    }
453    if (result.isUnsuccessfulRequest()) {
454      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
455    }
456    return (Parameters) result.getPayload();
457  }
458
459  public ValueSet expandValueset(ValueSet source, Parameters expParams, Map<String, String> params) {
460    Parameters p = expParams == null ? new Parameters() : expParams.copy();
461    if (source != null) {
462      p.addParameter().setName("valueSet").setResource(source);
463    }
464    if (params == null) {
465      params = new HashMap<>();
466    }
467    for (String n : params.keySet()) {
468      p.addParameter().setName(n).setValue(new StringType(params.get(n)));
469    }
470    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
471    try {
472
473      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
474        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
475        getPreferredResourceFormat(),
476        generateHeaders(),
477        source == null ? "ValueSet/$expand" : "ValueSet/$expand?url=" + source.getUrl(),
478        TIMEOUT_OPERATION_EXPAND);
479      if (result.isUnsuccessfulRequest()) {
480        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
481      }
482    } catch (IOException e) {
483      e.printStackTrace();
484    }
485    return result == null ? null : (ValueSet) result.getPayload();
486  }
487
488  public String getAddress() {
489    return base;
490  }
491
492  public ConceptMap initializeClosure(String name) {
493    Parameters params = new Parameters();
494    params.addParameter().setName("name").setValue(new StringType(name));
495    ResourceRequest<Resource> result = null;
496    try {
497      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
498        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
499        getPreferredResourceFormat(),
500        generateHeaders(),
501        "Closure?name=" + name,
502        TIMEOUT_NORMAL);
503      if (result.isUnsuccessfulRequest()) {
504        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
505      }
506    } catch (IOException e) {
507      e.printStackTrace();
508    }
509    return result == null ? null : (ConceptMap) result.getPayload();
510  }
511
512  public ConceptMap updateClosure(String name, Coding coding) {
513    Parameters params = new Parameters();
514    params.addParameter().setName("name").setValue(new StringType(name));
515    params.addParameter().setName("concept").setValue(coding);
516    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
517    try {
518      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
519        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
520        getPreferredResourceFormat(),
521        generateHeaders(),
522        "UpdateClosure?name=" + name,
523        TIMEOUT_OPERATION);
524      if (result.isUnsuccessfulRequest()) {
525        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
526      }
527    } catch (IOException e) {
528      e.printStackTrace();
529    }
530    return result == null ? null : (ConceptMap) result.getPayload();
531  }
532
533  public String getUsername() {
534    return username;
535  }
536
537  public void setUsername(String username) {
538    this.username = username;
539  }
540
541  public String getPassword() {
542    return password;
543  }
544
545  public void setPassword(String password) {
546    this.password = password;
547  }
548
549  public long getTimeout() {
550    return client.getTimeout();
551  }
552
553  public void setTimeout(long timeout) {
554    client.setTimeout(timeout);
555  }
556
557  public ToolingClientLogger getLogger() {
558    return client.getLogger();
559  }
560
561  public void setLogger(ToolingClientLogger logger) {
562    client.setLogger(logger);
563  }
564
565  public int getRetryCount() {
566    return client.getRetryCount();
567  }
568
569  public void setRetryCount(int retryCount) {
570    client.setRetryCount(retryCount);
571  }
572
573  public void setClientHeaders(ArrayList<Header> headers) {
574    this.headers = headers;
575  }
576
577  private Headers generateHeaders() {
578    Headers.Builder builder = new Headers.Builder();
579    // Add basic auth header if it exists
580    if (basicAuthHeaderExists()) {
581      builder.add(getAuthorizationHeader().toString());
582    }
583    // Add any other headers
584    if(this.headers != null) {
585      this.headers.forEach(header -> builder.add(header.toString()));
586    }
587    if (!Utilities.noString(userAgent)) {
588      builder.add("User-Agent: "+userAgent);
589    }
590
591    if (!Utilities.noString(acceptLang)) {
592      builder.add("Accept-Language: "+acceptLang);
593    }
594    
595    return builder.build();
596  }
597
598  public boolean basicAuthHeaderExists() {
599    return (username != null) && (password != null);
600  }
601
602  public Header getAuthorizationHeader() {
603    String usernamePassword = username + ":" + password;
604    String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
605    return new Header("Authorization", "Basic " + base64usernamePassword);
606  }
607
608  public String getUserAgent() {
609    return userAgent;
610  }
611
612  public void setUserAgent(String userAgent) {
613    this.userAgent = userAgent;
614  }
615
616  public String getServerVersion() {
617    if (capabilities == null) {
618      try {
619        getCapabilitiesStatementQuick();
620      } catch (Throwable e) {
621        //FIXME This is creepy. Shouldn't we report this at some level?
622      }
623    }
624    return capabilities == null ? null : capabilities.getSoftware().getVersion();
625  }
626
627  public void setLanguage(String lang) {
628    this.acceptLang = lang;
629  }
630  
631  
632}
633