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  //Pass endpoint for client - URI
107  public FHIRToolingClient(String baseServiceUrl, String userAgent) throws URISyntaxException {
108    preferredResourceFormat = ResourceFormat.RESOURCE_JSON;
109    this.userAgent = userAgent;
110    initialize(baseServiceUrl);
111  }
112
113  public void initialize(String baseServiceUrl) throws URISyntaxException {
114    base = baseServiceUrl;
115    resourceAddress = new ResourceAddress(baseServiceUrl);
116    this.maxResultSetSize = -1;
117  }
118
119  public Client getClient() {
120    return client;
121  }
122
123  public void setClient(Client client) {
124    this.client = client;
125  }
126
127  public String getPreferredResourceFormat() {
128    return preferredResourceFormat.getHeader();
129  }
130
131  public void setPreferredResourceFormat(ResourceFormat resourceFormat) {
132    preferredResourceFormat = resourceFormat;
133  }
134
135  public int getMaximumRecordCount() {
136    return maxResultSetSize;
137  }
138
139  public void setMaximumRecordCount(int maxResultSetSize) {
140    this.maxResultSetSize = maxResultSetSize;
141  }
142
143  private List<ResourceFormat> getResourceFormatsWithPreferredFirst() {
144    return Stream.concat(
145      Arrays.stream(new ResourceFormat[]{preferredResourceFormat}),
146      Arrays.stream(ResourceFormat.values()).filter(a -> a != preferredResourceFormat)
147    ).collect(Collectors.toList());
148  }
149
150  private <T extends Resource> T getCapabilities(URI resourceUri, String message, String exceptionMessage) throws FHIRException {
151    final List<ResourceFormat> resourceFormats = getResourceFormatsWithPreferredFirst();
152
153    for (ResourceFormat attemptedResourceFormat : resourceFormats) {
154      try {
155        T output =  (T) client.issueGetResourceRequest(resourceUri,
156          preferredResourceFormat.getHeader(),
157          generateHeaders(),
158          message,
159          TIMEOUT_NORMAL).getReference();
160        if (attemptedResourceFormat != preferredResourceFormat) {
161          setPreferredResourceFormat(attemptedResourceFormat);
162        }
163        return output;
164      } catch (Exception e) {
165        logger.warn("Failed attempt to fetch " + resourceUri, e);
166      }
167    }
168    throw new FHIRException(exceptionMessage);
169  }
170
171  public TerminologyCapabilities getTerminologyCapabilities() {
172    TerminologyCapabilities capabilities = null;
173
174    try {
175      capabilities = getCapabilities(resourceAddress.resolveMetadataTxCaps(),
176        "TerminologyCapabilities",
177        "Error fetching the server's terminology capabilities");
178    } catch (ClassCastException e) {
179      throw new FHIRException("Unexpected response format for Terminology Capability metadata", e);
180    }
181    return capabilities;
182  }
183
184  public CapabilityStatement getCapabilitiesStatement() {
185    CapabilityStatement capabilityStatement = null;
186
187      capabilityStatement = getCapabilities(resourceAddress.resolveMetadataUri(false),
188
189        "CapabilitiesStatement", "Error fetching the server's conformance statement");
190    return capabilityStatement;
191  }
192
193  public CapabilityStatement getCapabilitiesStatementQuick() throws EFhirClientException {
194    if (capabilities != null) return capabilities;
195
196       capabilities = getCapabilities(resourceAddress.resolveMetadataUri(true),
197
198        "CapabilitiesStatement-Quick",
199        "Error fetching the server's capability statement");
200
201    return capabilities;
202  }
203
204  public <T extends Resource> T read(Class<T> resourceClass, String id) {//TODO Change this to AddressableResource
205    ResourceRequest<T> result = null;
206    try {
207      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
208        getPreferredResourceFormat(),
209        generateHeaders(),
210        "Read " + resourceClass.getName() + "/" + id,
211        TIMEOUT_NORMAL);
212      if (result.isUnsuccessfulRequest()) {
213        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
214      }
215    } catch (Exception e) {
216      throw new FHIRException(e);
217    }
218    return result.getPayload();
219  }
220
221  public <T extends Resource> T vread(Class<T> resourceClass, String id, String version) {
222    ResourceRequest<T> result = null;
223    try {
224      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndIdAndVersion(resourceClass, id, version),
225        getPreferredResourceFormat(),
226        generateHeaders(),
227        "VRead " + resourceClass.getName() + "/" + id + "/?_history/" + version,
228        TIMEOUT_NORMAL);
229      if (result.isUnsuccessfulRequest()) {
230        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
231      }
232    } catch (Exception e) {
233      throw new FHIRException("Error trying to read this version of the resource", e);
234    }
235    return result.getPayload();
236  }
237
238  public <T extends Resource> T getCanonical(Class<T> resourceClass, String canonicalURL) {
239    ResourceRequest<T> result = null;
240    try {
241      result = client.issueGetResourceRequest(resourceAddress.resolveGetUriFromResourceClassAndCanonical(resourceClass, canonicalURL),
242        getPreferredResourceFormat(),
243        generateHeaders(),
244        "Read " + resourceClass.getName() + "?url=" + canonicalURL,
245        TIMEOUT_NORMAL);
246      if (result.isUnsuccessfulRequest()) {
247        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
248      }
249    } catch (Exception e) {
250      handleException("An error has occurred while trying to read this version of the resource", e);
251    }
252    Bundle bnd = (Bundle) result.getPayload();
253    if (bnd.getEntry().size() == 0)
254      throw new EFhirClientException("No matching resource found for canonical URL '" + canonicalURL + "'");
255    if (bnd.getEntry().size() > 1)
256      throw new EFhirClientException("Multiple matching resources found for canonical URL '" + canonicalURL + "'");
257    return (T) bnd.getEntry().get(0).getResource();
258  }
259
260  public Resource update(Resource resource) {
261    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
262    try {
263      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resource.getClass(), resource.getId()),
264        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
265        getPreferredResourceFormat(),
266        generateHeaders(),
267        "Update " + resource.fhirType() + "/" + resource.getId(),
268        TIMEOUT_OPERATION);
269      if (result.isUnsuccessfulRequest()) {
270        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
271      }
272    } catch (Exception e) {
273      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
274    }
275    // 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
276    try {
277      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
278      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
279      return this.vread(resource.getClass(), resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
280    } catch (ClassCastException e) {
281      // if we fall throught we have the correct type already in the create
282    }
283
284    return result.getPayload();
285  }
286
287  public <T extends Resource> T update(Class<T> resourceClass, T resource, String id) {
288    ResourceRequest<T> result = null;
289    try {
290      result = client.issuePutRequest(resourceAddress.resolveGetUriFromResourceClassAndId(resourceClass, id),
291        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
292        getPreferredResourceFormat(),
293        generateHeaders(),
294        "Update " + resource.fhirType() + "/" + id,
295        TIMEOUT_OPERATION);
296      if (result.isUnsuccessfulRequest()) {
297        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
298      }
299    } catch (Exception e) {
300      throw new EFhirClientException("An error has occurred while trying to update this resource", e);
301    }
302    // 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
303    try {
304      OperationOutcome operationOutcome = (OperationOutcome) result.getPayload();
305      ResourceAddress.ResourceVersionedIdentifier resVersionedIdentifier = ResourceAddress.parseCreateLocation(result.getLocation());
306      return this.vread(resourceClass, resVersionedIdentifier.getId(), resVersionedIdentifier.getVersionId());
307    } catch (ClassCastException e) {
308      // if we fall through we have the correct type already in the create
309    }
310
311    return result.getPayload();
312  }
313
314  public <T extends Resource> Parameters operateType(Class<T> resourceClass, String name, Parameters params) {
315    boolean complex = false;
316    for (ParametersParameterComponent p : params.getParameter())
317      complex = complex || !(p.getValue() instanceof PrimitiveType);
318    String ps = "";
319    try {
320      if (!complex)
321        for (ParametersParameterComponent p : params.getParameter())
322          if (p.getValue() instanceof PrimitiveType)
323            ps += p.getName() + "=" + Utilities.encodeUri(((PrimitiveType) p.getValue()).asStringValue()) + "&";
324      ResourceRequest<T> result;
325      URI url = resourceAddress.resolveOperationURLFromClass(resourceClass, name, ps);
326      if (complex) {
327        byte[] body = ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat()));
328        result = client.issuePostRequest(url, body, getPreferredResourceFormat(), generateHeaders(),
329            "POST " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
330      } else {
331        result = client.issueGetResourceRequest(url, getPreferredResourceFormat(), generateHeaders(), "GET " + resourceClass.getName() + "/$" + name, TIMEOUT_OPERATION_LONG);
332      }
333      if (result.isUnsuccessfulRequest()) {
334        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
335      }
336      if (result.getPayload() instanceof Parameters) {
337        return (Parameters) result.getPayload();
338      } else {
339        Parameters p_out = new Parameters();
340        p_out.addParameter().setName("return").setResource(result.getPayload());
341        return p_out;
342      }
343    } catch (Exception e) {
344      handleException("Error performing tx5 operation '"+name+": "+e.getMessage()+"' (parameters = \"" + ps+"\")", e);                  
345    }
346    return null;
347  }
348
349  public Bundle transaction(Bundle batch) {
350    Bundle transactionResult = null;
351    try {
352      transactionResult = client.postBatchRequest(resourceAddress.getBaseServiceUri(), ByteUtils.resourceToByteArray(batch, false, isJson(getPreferredResourceFormat())), getPreferredResourceFormat(),
353          generateHeaders(),
354          "transaction", TIMEOUT_OPERATION + (TIMEOUT_ENTRY * batch.getEntry().size()));
355    } catch (Exception e) {
356      handleException("An error occurred trying to process this transaction request", e);
357    }
358    return transactionResult;
359  }
360
361  @SuppressWarnings("unchecked")
362  public <T extends Resource> OperationOutcome validate(Class<T> resourceClass, T resource, String id) {
363    ResourceRequest<T> result = null;
364    try {
365      result = client.issuePostRequest(resourceAddress.resolveValidateUri(resourceClass, id),
366        ByteUtils.resourceToByteArray(resource, false, isJson(getPreferredResourceFormat())),
367        getPreferredResourceFormat(), generateHeaders(),
368        "POST " + resourceClass.getName() + (id != null ? "/" + id : "") + "/$validate", TIMEOUT_OPERATION_LONG);
369      if (result.isUnsuccessfulRequest()) {
370        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
371      }
372    } catch (Exception e) {
373      handleException("An error has occurred while trying to validate this resource", e);
374    }
375    return (OperationOutcome) result.getPayload();
376  }
377
378  /**
379   * Helper method to prevent nesting of previously thrown EFhirClientExceptions
380   *
381   * @param e
382   * @throws EFhirClientException
383   */
384  protected void handleException(String message, Exception e) throws EFhirClientException {
385    if (e instanceof EFhirClientException) {
386      throw (EFhirClientException) e;
387    } else {
388      throw new EFhirClientException(message, e);
389    }
390  }
391
392  /**
393   * Helper method to determine whether desired resource representation
394   * is Json or XML.
395   *
396   * @param format
397   * @return
398   */
399  protected boolean isJson(String format) {
400    boolean isJson = false;
401    if (format.toLowerCase().contains("json")) {
402      isJson = true;
403    }
404    return isJson;
405  }
406
407  public Bundle fetchFeed(String url) {
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 ValueSet expandValueset(ValueSet source, Parameters expParams) {
418    Parameters p = expParams == null ? new Parameters() : expParams.copy();
419    p.addParameter().setName("valueSet").setResource(source);
420    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
421    try {
422      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"),
423        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
424        getPreferredResourceFormat(),
425        generateHeaders(),
426        "ValueSet/$expand?url=" + source.getUrl(),
427        TIMEOUT_OPERATION_EXPAND);
428      if (result.isUnsuccessfulRequest()) {
429        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
430      }
431    } catch (IOException e) {
432      e.printStackTrace();
433    }
434    return result == null ? null : (ValueSet) result.getPayload();
435  }
436
437
438  public Parameters lookupCode(Map<String, String> params) {
439    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
440    try {
441      result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params),
442        getPreferredResourceFormat(),
443        generateHeaders(),
444        "CodeSystem/$lookup",
445        TIMEOUT_NORMAL);
446    } catch (IOException e) {
447      e.printStackTrace();
448    }
449    if (result.isUnsuccessfulRequest()) {
450      throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
451    }
452    return (Parameters) result.getPayload();
453  }
454
455  public ValueSet expandValueset(ValueSet source, Parameters expParams, Map<String, String> params) {
456    Parameters p = expParams == null ? new Parameters() : expParams.copy();
457    p.addParameter().setName("valueSet").setResource(source);
458    for (String n : params.keySet()) {
459      p.addParameter().setName(n).setValue(new StringType(params.get(n)));
460    }
461    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
462    try {
463
464      result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params),
465        ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())),
466        getPreferredResourceFormat(),
467        generateHeaders(),
468        "ValueSet/$expand?url=" + source.getUrl(),
469        TIMEOUT_OPERATION_EXPAND);
470      if (result.isUnsuccessfulRequest()) {
471        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
472      }
473    } catch (IOException e) {
474      e.printStackTrace();
475    }
476    return result == null ? null : (ValueSet) result.getPayload();
477  }
478
479  public String getAddress() {
480    return base;
481  }
482
483  public ConceptMap initializeClosure(String name) {
484    Parameters params = new Parameters();
485    params.addParameter().setName("name").setValue(new StringType(name));
486    ResourceRequest<Resource> result = null;
487    try {
488      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
489        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
490        getPreferredResourceFormat(),
491        generateHeaders(),
492        "Closure?name=" + name,
493        TIMEOUT_NORMAL);
494      if (result.isUnsuccessfulRequest()) {
495        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
496      }
497    } catch (IOException e) {
498      e.printStackTrace();
499    }
500    return result == null ? null : (ConceptMap) result.getPayload();
501  }
502
503  public ConceptMap updateClosure(String name, Coding coding) {
504    Parameters params = new Parameters();
505    params.addParameter().setName("name").setValue(new StringType(name));
506    params.addParameter().setName("concept").setValue(coding);
507    org.hl7.fhir.r5.utils.client.network.ResourceRequest<Resource> result = null;
508    try {
509      result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()),
510        ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())),
511        getPreferredResourceFormat(),
512        generateHeaders(),
513        "UpdateClosure?name=" + name,
514        TIMEOUT_OPERATION);
515      if (result.isUnsuccessfulRequest()) {
516        throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload());
517      }
518    } catch (IOException e) {
519      e.printStackTrace();
520    }
521    return result == null ? null : (ConceptMap) result.getPayload();
522  }
523
524  public String getUsername() {
525    return username;
526  }
527
528  public void setUsername(String username) {
529    this.username = username;
530  }
531
532  public String getPassword() {
533    return password;
534  }
535
536  public void setPassword(String password) {
537    this.password = password;
538  }
539
540  public long getTimeout() {
541    return client.getTimeout();
542  }
543
544  public void setTimeout(long timeout) {
545    client.setTimeout(timeout);
546  }
547
548  public ToolingClientLogger getLogger() {
549    return client.getLogger();
550  }
551
552  public void setLogger(ToolingClientLogger logger) {
553    client.setLogger(logger);
554  }
555
556  public int getRetryCount() {
557    return client.getRetryCount();
558  }
559
560  public void setRetryCount(int retryCount) {
561    client.setRetryCount(retryCount);
562  }
563
564  public void setClientHeaders(ArrayList<Header> headers) {
565    this.headers = headers;
566  }
567
568  private Headers generateHeaders() {
569    Headers.Builder builder = new Headers.Builder();
570    // Add basic auth header if it exists
571    if (basicAuthHeaderExists()) {
572      builder.add(getAuthorizationHeader().toString());
573    }
574    // Add any other headers
575    if(this.headers != null) {
576      this.headers.forEach(header -> builder.add(header.toString()));
577    }
578    if (!Utilities.noString(userAgent)) {
579      builder.add("User-Agent: "+userAgent);
580    }
581    return builder.build();
582  }
583
584  public boolean basicAuthHeaderExists() {
585    return (username != null) && (password != null);
586  }
587
588  public Header getAuthorizationHeader() {
589    String usernamePassword = username + ":" + password;
590    String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
591    return new Header("Authorization", "Basic " + base64usernamePassword);
592  }
593
594  public String getUserAgent() {
595    return userAgent;
596  }
597
598  public void setUserAgent(String userAgent) {
599    this.userAgent = userAgent;
600  }
601
602  public String getServerVersion() {
603    if (capabilities == null) {
604      try {
605        getCapabilitiesStatementQuick();
606      } catch (Throwable e) {
607        //FIXME This is creepy. Shouldn't we report this at some level?
608      }
609    }
610    return capabilities == null ? null : capabilities.getSoftware().getVersion();
611  }
612  
613  
614}
615