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