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