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