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