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 checkCapabilities(); 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(ValueSet source, Parameters expParams) { 368 Parameters p = expParams == null ? new Parameters() : expParams.copy(); 369 p.addParameter().setName("valueSet").setResource(source); 370 org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null; 371 try { 372 result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand"), 373 ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())), 374 getPreferredResourceFormat(), 375 generateHeaders(), 376 "ValueSet/$expand?url=" + source.getUrl(), 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 Parameters lookupCode(Map<String, String> params) { 389 org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null; 390 try { 391 result = client.issueGetResourceRequest(resourceAddress.resolveOperationUri(CodeSystem.class, "lookup", params), 392 getPreferredResourceFormat(), 393 generateHeaders(), 394 "CodeSystem/$lookup", 395 TIMEOUT_NORMAL); 396 } catch (IOException e) { 397 e.printStackTrace(); 398 } 399 if (result.isUnsuccessfulRequest()) { 400 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 401 } 402 return (Parameters) result.getPayload(); 403 } 404 405 public ValueSet expandValueset(ValueSet source, Parameters expParams, Map<String, String> params) { 406 Parameters p = expParams == null ? new Parameters() : expParams.copy(); 407 p.addParameter().setName("valueSet").setResource(source); 408 for (String n : params.keySet()) { 409 p.addParameter().setName(n).setValue(new StringType(params.get(n))); 410 } 411 org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null; 412 try { 413 result = client.issuePostRequest(resourceAddress.resolveOperationUri(ValueSet.class, "expand", params), 414 ByteUtils.resourceToByteArray(p, false, isJson(getPreferredResourceFormat())), 415 getPreferredResourceFormat(), 416 generateHeaders(), 417 "ValueSet/$expand?url=" + source.getUrl(), 418 TIMEOUT_OPERATION_EXPAND); 419 if (result.isUnsuccessfulRequest()) { 420 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 421 } 422 } catch (IOException e) { 423 e.printStackTrace(); 424 } 425 return result == null ? null : (ValueSet) result.getPayload(); 426 } 427 428 public String getAddress() { 429 return base; 430 } 431 432 public ConceptMap initializeClosure(String name) { 433 Parameters params = new Parameters(); 434 params.addParameter().setName("name").setValue(new StringType(name)); 435 ResourceRequest<Resource> result = null; 436 try { 437 result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()), 438 ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), 439 getPreferredResourceFormat(), 440 generateHeaders(), 441 "Closure?name=" + name, 442 TIMEOUT_NORMAL); 443 if (result.isUnsuccessfulRequest()) { 444 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 445 } 446 } catch (IOException e) { 447 e.printStackTrace(); 448 } 449 return result == null ? null : (ConceptMap) result.getPayload(); 450 } 451 452 public ConceptMap updateClosure(String name, Coding coding) { 453 Parameters params = new Parameters(); 454 params.addParameter().setName("name").setValue(new StringType(name)); 455 params.addParameter().setName("concept").setValue(coding); 456 org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null; 457 try { 458 result = client.issuePostRequest(resourceAddress.resolveOperationUri(null, "closure", new HashMap<String, String>()), 459 ByteUtils.resourceToByteArray(params, false, isJson(getPreferredResourceFormat())), 460 getPreferredResourceFormat(), 461 generateHeaders(), 462 "UpdateClosure?name=" + name, 463 TIMEOUT_OPERATION); 464 if (result.isUnsuccessfulRequest()) { 465 throw new EFhirClientException("Server returned error code " + result.getHttpStatus(), (OperationOutcome) result.getPayload()); 466 } 467 } catch (IOException e) { 468 e.printStackTrace(); 469 } 470 return result == null ? null : (ConceptMap) result.getPayload(); 471 } 472 473 public String getUsername() { 474 return username; 475 } 476 477 public void setUsername(String username) { 478 this.username = username; 479 } 480 481 public String getPassword() { 482 return password; 483 } 484 485 public void setPassword(String password) { 486 this.password = password; 487 } 488 489 public long getTimeout() { 490 return client.getTimeout(); 491 } 492 493 public void setTimeout(long timeout) { 494 client.setTimeout(timeout); 495 } 496 497 public ToolingClientLogger getLogger() { 498 return client.getLogger(); 499 } 500 501 public void setLogger(ToolingClientLogger logger) { 502 client.setLogger(logger); 503 } 504 505 public int getRetryCount() { 506 return client.getRetryCount(); 507 } 508 509 public void setRetryCount(int retryCount) { 510 client.setRetryCount(retryCount); 511 } 512 513 public void setClientHeaders(ArrayList<Header> headers) { 514 this.headers = headers; 515 } 516 517 private Headers generateHeaders() { 518 Headers.Builder builder = new Headers.Builder(); 519 // Add basic auth header if it exists 520 if (basicAuthHeaderExists()) { 521 builder.add(getAuthorizationHeader().toString()); 522 } 523 // Add any other headers 524 if(this.headers != null) { 525 this.headers.forEach(header -> builder.add(header.toString())); 526 } 527 if (!Utilities.noString(userAgent)) { 528 builder.add("User-Agent: "+userAgent); 529 } 530 return builder.build(); 531 } 532 533 public boolean basicAuthHeaderExists() { 534 return (username != null) && (password != null); 535 } 536 537 public Header getAuthorizationHeader() { 538 String usernamePassword = username + ":" + password; 539 String base64usernamePassword = Base64.getEncoder().encodeToString(usernamePassword.getBytes()); 540 return new Header("Authorization", "Basic " + base64usernamePassword); 541 } 542 543 public String getUserAgent() { 544 return userAgent; 545 } 546 547 public void setUserAgent(String userAgent) { 548 this.userAgent = userAgent; 549 } 550 551 public String getServerVersion() { 552 return capabilities == null ? null : capabilities.getSoftware().getVersion(); 553 } 554} 555