
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_XML; 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 for (String n : params.keySet()) { 429 p.addParameter().setName(n).setValue(new StringType(params.get(n))); 430 } 431 org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null; 432 try { 433 result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params), 434 ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())), 435 getPreferredResourceFormat(), 436 generateHeaders(), 437 "ValueSet/$expand?url=" + source.getUrl(), 438 TIMEOUT_OPERATION_EXPAND); 439 if (result.isUnsuccessfulRequest()) { 440 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 441 } 442 } catch (IOException e) { 443 e.printStackTrace(); 444 } 445 return result == null ? null : (ValueSet) result.getPayload(); 446 } 447 448 public String getAddress() { 449 return base; 450 } 451 452 public ConceptMap initializeClosure(String name) { 453 Parameters params = new Parameters(); 454 params.addParameter().setName("name").setValue(new StringType(name)); 455 ResourceRequest<Resource> result = null; 456 try { 457 result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()), 458 ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), 459 getPreferredResourceFormat(), 460 generateHeaders(), 461 "Closure?name=" + name, 462 TIMEOUT_NORMAL); 463 if (result.isUnsuccessfulRequest()) { 464 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 465 } 466 } catch (IOException e) { 467 e.printStackTrace(); 468 } 469 return result == null ? null : (ConceptMap) result.getPayload(); 470 } 471 472 public ConceptMap updateClosure(String name, Coding coding) { 473 Parameters params = new Parameters(); 474 params.addParameter().setName("name").setValue(new StringType(name)); 475 params.addParameter().setName("concept").setValue(coding); 476 org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null; 477 try { 478 result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()), 479 ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), 480 getPreferredResourceFormat(), 481 generateHeaders(), 482 "UpdateClosure?name=" + name, 483 TIMEOUT_OPERATION); 484 if (result.isUnsuccessfulRequest()) { 485 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 486 } 487 } catch (IOException e) { 488 e.printStackTrace(); 489 } 490 return result == null ? null : (ConceptMap) result.getPayload(); 491 } 492 493 public String getUsername() { 494 return username; 495 } 496 497 public void setUsername(String username) { 498 this.username = username; 499 } 500 501 public String getPassword() { 502 return password; 503 } 504 505 public void setPassword(String password) { 506 this.password = password; 507 } 508 509 public long getTimeout() { 510 return client.getTimeout(); 511 } 512 513 public void setTimeout(long timeout) { 514 client.setTimeout(timeout); 515 } 516 517 public ToolingClientLogger getLogger() { 518 return client.getLogger(); 519 } 520 521 public void setLogger(ToolingClientLogger logger) { 522 client.setLogger(logger); 523 } 524 525 public int getRetryCount() { 526 return client.getRetryCount(); 527 } 528 529 public void setRetryCount(int retryCount) { 530 client.setRetryCount(retryCount); 531 } 532 533 public void setClientHeaders(ArrayList<Header> headers) { 534 this.headers = headers; 535 } 536 537 private Headers generateHeaders() { 538 Headers.Builder builder = new Headers.Builder(); 539 // Add basic auth header if it exists 540 if (basicAuthHeaderExists()) { 541 builder.add(getAuthorizationHeader().toString()); 542 } 543 // Add any other headers 544 if(this.headers != null) { 545 this.headers.forEach(header -> builder.add(header.toString())); 546 } 547 if (!Utilities.noString(userAgent)) { 548 builder.add("User-Agent: "+userAgent); 549 } 550 return builder.build(); 551 } 552 553 public boolean basicAuthHeaderExists() { 554 return (username != null) && (password != null); 555 } 556 557 public Header getAuthorizationHeader() { 558 String usernamePassword = username + ":" + password; 559 String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes()); 560 return new Header("Authorization", "Basic " + base64usernamePassword); 561 } 562 563 public String getUserAgent() { 564 return userAgent; 565 } 566 567 public void setUserAgent(String userAgent) { 568 this.userAgent = userAgent; 569 } 570 571 public String getServerVersion() { 572 checkCapabilities(); 573 return capabilities == null ? null : capabilities.getSoftware().getVersion(); 574 } 575} 576