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