001package ca.cdr.test.app.clients; 002/*- 003 * #%L 004 * Smile CDR - CDR 005 * %% 006 * Copyright (C) 2016 - 2025 Smile CDR, Inc. 007 * %% 008 * All rights reserved. 009 * #L% 010 */ 011 012import ca.cdr.api.model.json.OAuth2ClientDetailsJson; 013import ca.cdr.api.model.json.OAuth2WritableClientDetailsJson; 014import ca.cdr.api.model.json.TransactionLogEventsJson; 015import ca.cdr.api.model.json.UserDetailsJson; 016import ca.cdr.test.model.NodeConfigurations; 017import ca.uhn.fhir.context.FhirContext; 018import ca.uhn.fhir.context.FhirVersionEnum; 019import com.fasterxml.jackson.core.JsonProcessingException; 020import com.fasterxml.jackson.databind.JsonNode; 021import com.fasterxml.jackson.databind.ObjectMapper; 022import jakarta.annotation.Nonnull; 023import org.apache.commons.lang3.Validate; 024import org.hl7.fhir.instance.model.api.IBaseParameters; 025import org.hl7.fhir.instance.model.api.IBaseResource; 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028import org.springframework.http.MediaType; 029import org.springframework.http.client.support.BasicAuthenticationInterceptor; 030import org.springframework.web.client.RestClient; 031 032import java.util.Map; 033import java.util.Objects; 034 035// Many new methods generated by Claude Sonnet 3.7 036 037/** 038 * AdminJsonRestClient is a client for the Admin API of the CDR. 039 * It currently supports anonymous and http-basic authentication. 040 */ 041public class AdminJsonRestClient { 042 private static final Logger ourLog = LoggerFactory.getLogger(AdminJsonRestClient.class); 043 044 final RestClient myRestClient; 045 046 AdminJsonRestClient(RestClient theRestClient) { 047 myRestClient = theRestClient; 048 } 049 050 public static AdminJsonRestClient build(String theBaseUrl, String theUsername, String thePassword) { 051 RestClient restClient = builderForUrlWithJsonDefault(theBaseUrl) 052 .requestInterceptor(new BasicAuthenticationInterceptor(theUsername, thePassword)) 053 .build(); 054 055 return new AdminJsonRestClient(restClient); 056 } 057 058 public static AdminJsonRestClient buildAnonymous(String theBaseUrl) { 059 RestClient restClient = builderForUrlWithJsonDefault(theBaseUrl).build(); 060 061 return new AdminJsonRestClient(restClient); 062 } 063 064 private static @Nonnull RestClient.Builder builderForUrlWithJsonDefault(String theBaseUrl) { 065 return RestClient.builder() 066 .baseUrl(theBaseUrl) 067 // This sets the default content type to JSON, but allows the actual request to override it. 068 .defaultRequest(r -> r.accept(MediaType.APPLICATION_JSON)); 069 } 070 071 072 @Nonnull 073 public UserDetailsJson userCreate(String theNode, String theModuleId, UserDetailsJson userDetails) { 074 Validate.notEmpty(theNode, "Node ID is required"); 075 Validate.notEmpty(theModuleId, "Module ID is required"); 076 Validate.notNull(userDetails, "User does not have an assigned pid. Use userCreate() to create users."); 077 UserDetailsJson result = myRestClient 078 .post() 079 .uri("user-management/{nodeId}/{moduleId}", theNode, theModuleId) 080 .body(userDetails) 081 .retrieve() 082 .body(UserDetailsJson.class); 083 return Objects.requireNonNull(result); 084 } 085 086 @Nonnull 087 public UserDetailsJson userUpdate(UserDetailsJson theUserDetails) { 088 Validate.notEmpty(theUserDetails.getNodeId(), "Node ID is required"); 089 Validate.notEmpty(theUserDetails.getModuleId(), "Module ID is required"); 090 Validate.notNull( 091 theUserDetails.getPid(), "User does not have an assigned pid. Use userCreate() to create users."); 092 UserDetailsJson result = myRestClient 093 .put() 094 .uri( 095 "user-management/{nodeId}/{moduleId}/{userId}", 096 theUserDetails.getNodeId(), 097 theUserDetails.getModuleId(), 098 theUserDetails.getPid()) 099 .body(theUserDetails) 100 .retrieve() 101 .body(UserDetailsJson.class); 102 return Objects.requireNonNull(result); 103 } 104 105 @Nonnull 106 public JsonNode userFindByUsername(String theNodeId, String theModuleId, String theUsername) { 107 Validate.notEmpty(theNodeId, "Node ID is required"); 108 Validate.notEmpty(theModuleId, "Module ID is required"); 109 Validate.notEmpty(theUsername, "username is required"); 110 JsonNode result = myRestClient 111 .get() 112 .uri("user-management/{nodeId}/{moduleId}?searchTerm={username}", theNodeId, theModuleId, theUsername) 113 .retrieve() 114 .body(JsonNode.class); 115 return Objects.requireNonNull(result); 116 } 117 118 /** 119 * Find users by username and return the result as a String that can be used with ObjectMapper 120 * 121 * @param theNodeId The node ID 122 * @param theModuleId The module ID 123 * @param theUsername The username to search for 124 * @return The JSON string representation of the result 125 */ 126 @Nonnull 127 public String userFindByUsernameAsString(String theNodeId, String theModuleId, String theUsername) { 128 JsonNode result = userFindByUsername(theNodeId, theModuleId, theUsername); 129 return result.toString(); 130 } 131 132 @Nonnull 133 public JsonNode userFindAll(String theNodeId, String theModuleId, int thePageSize) { 134 Validate.notEmpty(theNodeId, "Node ID is required"); 135 Validate.notEmpty(theModuleId, "Module ID is required"); 136 JsonNode result = myRestClient 137 .get() 138 .uri("user-management/{nodeId}/{moduleId}?pageSize={pageSize}", theNodeId, theModuleId, thePageSize) 139 .retrieve() 140 .body(JsonNode.class); 141 return Objects.requireNonNull(result); 142 } 143 144 /** 145 * Find all users and return the result as a String that can be used with ObjectMapper 146 * 147 * @param theNodeId The node ID 148 * @param theModuleId The module ID 149 * @param thePageSize The page size 150 * @return The JSON string representation of the result 151 */ 152 @Nonnull 153 public String userFindAllAsString(String theNodeId, String theModuleId, int thePageSize) { 154 JsonNode result = userFindAll(theNodeId, theModuleId, thePageSize); 155 return result.toString(); 156 } 157 158 @Nonnull 159 public UserDetailsJson userFetchByPid(String theNodeId, String theModuleId, Long thePid) { 160 Validate.notEmpty(theNodeId, "Node ID is required"); 161 Validate.notEmpty(theModuleId, "Module ID is required"); 162 Validate.notNull(thePid, "User does not have an assigned pid. Use userCreate() to create users."); 163 UserDetailsJson result = myRestClient 164 .get() 165 .uri("user-management/{nodeId}/{moduleId}/{userId}", theNodeId, theModuleId, thePid) 166 .retrieve() 167 .body(UserDetailsJson.class); 168 return Objects.requireNonNull(result); 169 } 170 171 /** 172 * Get the module configuration for a specific node and module 173 */ 174 @Nonnull 175 public JsonNode getModuleConfig(String theNodeId, String theModuleId) { 176 Validate.notEmpty(theNodeId, "Node ID is required"); 177 Validate.notEmpty(theModuleId, "Module ID is required"); 178 JsonNode result = myRestClient 179 .get() 180 .uri("module-config/{nodeId}/{moduleId}", theNodeId, theModuleId) 181 .retrieve() 182 .body(JsonNode.class); 183 return Objects.requireNonNull(result); 184 } 185 186 /** 187 * Update the configuration for a specific module 188 */ 189 @Nonnull 190 public JsonNode updateModuleConfig( 191 String theNodeId, 192 String theModuleId, 193 JsonNode theOptions, 194 boolean theRestart, 195 boolean theReload) { 196 Validate.notEmpty(theNodeId, "Node ID is required"); 197 Validate.notEmpty(theModuleId, "Module ID is required"); 198 Validate.notNull(theOptions, "Options is required"); 199 200 JsonNode result = 201 myRestClient.put().uri("module-config/{nodeId}/{theModuleId}/set?restart={theRestart}&reload={theReload}", theNodeId, theModuleId, theRestart, theReload).body(theOptions).retrieve().body(JsonNode.class); 202 return Objects.requireNonNull(result); 203 } 204 205 /** 206 * Request to start a module on all processes 207 */ 208 @Nonnull 209 private JsonNode startModule(String theNodeId, String theModuleId) { 210 Validate.notEmpty(theNodeId, "Node ID is required"); 211 Validate.notEmpty(theModuleId, "Module ID is required"); 212 JsonNode result = myRestClient 213 .post() 214 .uri("module-config/{nodeId}/{moduleId}/start", theNodeId, theModuleId) 215 .retrieve() 216 .body(JsonNode.class); 217 return Objects.requireNonNull(result); 218 } 219 220 /** 221 * Request to stop a module on all processes 222 * ModuleProcessesStatusChangeResponseJson 223 */ 224 @Nonnull 225 public JsonNode stopModule(String theNodeId, String theModuleId) { 226 Validate.notEmpty(theNodeId, "Node ID is required"); 227 Validate.notEmpty(theModuleId, "Module ID is required"); 228 JsonNode result = myRestClient 229 .post() 230 .uri("module-config/{nodeId}/{moduleId}/stop", theNodeId, theModuleId) 231 .retrieve() 232 .body(JsonNode.class); 233 return Objects.requireNonNull(result); 234 } 235 236 /** 237 * Request to restart a module on all processes 238 */ 239 @Nonnull 240 public JsonNode restartModule(String theNodeId, String theModuleId) { 241 Validate.notEmpty(theNodeId, "Node ID is required"); 242 Validate.notEmpty(theModuleId, "Module ID is required"); 243 JsonNode result = myRestClient 244 .post() 245 .uri("module-config/{nodeId}/{moduleId}/restart", theNodeId, theModuleId) 246 .retrieve() 247 .body(JsonNode.class); 248 return Objects.requireNonNull(result); 249 } 250 251 /** 252 * Get a list of restore points for a node 253 */ 254 @Nonnull 255 public JsonNode getRestorePoints( 256 String theNodeId, String theVersion, String theFromDate, String theToDate, int theOffset, int theCount) { 257 Validate.notEmpty(theNodeId, "Node ID is required"); 258 259 JsonNode result = myRestClient.get() 260 .uri("module-config/{theNodeId}/restorePoints", builder->{ 261 if (theVersion != null) { 262 builder.queryParam("version", theVersion); 263 } 264 if (theFromDate != null) { 265 builder.queryParam("from", theFromDate); 266 } 267 if (theToDate != null) { 268 builder.queryParam("to", theToDate); 269 } 270 builder.queryParam("offset", theOffset); 271 builder.queryParam("count", theCount); 272 return builder.build(theNodeId); 273 }).retrieve().body(JsonNode.class); 274 275 return Objects.requireNonNull(result); 276 } 277 278 /** 279 * Get a specific restore point 280 */ 281 @Nonnull 282 public JsonNode getRestorePoint(String theNodeId, Long theId) { 283 Validate.notEmpty(theNodeId, "Node ID is required"); 284 Validate.notNull(theId, "Restore point ID is required"); 285 JsonNode result = myRestClient 286 .get() 287 .uri("module-config/{nodeId}/restorePoints/{id}", theNodeId, theId) 288 .retrieve() 289 .body(JsonNode.class); 290 return Objects.requireNonNull(result); 291 } 292 293 /** 294 * Restore the system to a specific restore point 295 */ 296 public void restoreSystem(String theNodeId, Long theRestorePointId) { 297 Validate.notEmpty(theNodeId, "Node ID is required"); 298 Validate.notNull(theRestorePointId, "Restore point ID is required"); 299 myRestClient 300 .post() 301 .uri("module-config/{nodeId}/restorePoints/{id}/restore", theNodeId, theRestorePointId) 302 .retrieve() 303 .toBodilessEntity(); 304 } 305 306 /** 307 * Get health checks for all modules 308 */ 309 @Nonnull 310 public JsonNode getHealthChecks(boolean theOnlyRunning) { 311 JsonNode result = myRestClient.get().uri("runtime-status/node-statuses/health-checks?onlyRunning={onlyRunning}", theOnlyRunning).retrieve().body(JsonNode.class); 312 return Objects.requireNonNull(result); 313 } 314 315 /** 316 * Get node statuses 317 */ 318 @Nonnull 319 public JsonNode getNodeStatuses() { 320 JsonNode result = myRestClient 321 .get() 322 .uri("runtime-status/node-statuses/complete") 323 .retrieve() 324 .body(JsonNode.class); 325 return Objects.requireNonNull(result); 326 } 327 328 /** 329 * Cancel a batch job 330 */ 331 public void cancelBatchJob(String theModuleId, String theJobId) { 332 Validate.notEmpty(theModuleId, "Module ID is required"); 333 Validate.notEmpty(theJobId, "Job ID is required"); 334 myRestClient 335 .get() 336 .uri("batch2-jobs/modules/{moduleId}/jobs/{jobId}/cancel", theModuleId, theJobId) 337 .retrieve() 338 .toBodilessEntity(); 339 } 340 341 /** 342 * Process a bulk ETL import file 343 */ 344 public String processBulkImport(String theModuleId, String theFilename, String theContent) { 345 Validate.notEmpty(theModuleId, "Module ID is required"); 346 Validate.notEmpty(theFilename, "Filename is required"); 347 Validate.notNull(theContent, "Content is required"); 348 349 String result = myRestClient 350 .post() 351 .uri("bulk-import/process-etl-file/{moduleId}?filename={filename}", theModuleId, theFilename) 352 .accept(MediaType.TEXT_PLAIN) 353 .body(theContent) 354 .retrieve() 355 .body(String.class); 356 357 return result; 358 } 359 360 /** 361 * Get transaction logs 362 */ 363 @Nonnull 364 public TransactionLogEventsJson getTransactionLogs() { 365 TransactionLogEventsJson result = 366 myRestClient.get().uri("transaction-log").retrieve().body(TransactionLogEventsJson.class); 367 return Objects.requireNonNull(result); 368 } 369 370 /** 371 * Get a specific transaction log event with its body 372 */ 373 @Nonnull 374 public TransactionLogEventsJson.TransactionLogEventJson getTransactionLogEvent( 375 Long theEventId, boolean theIncludeBody) { 376 Validate.notNull(theEventId, "Event ID is required"); 377 378 TransactionLogEventsJson.TransactionLogEventJson result = 379 myRestClient.get().uri("transaction-log/event/{theEventId}?includeBody={includeBody}", theEventId, theIncludeBody).retrieve().body(TransactionLogEventsJson.TransactionLogEventJson.class); 380 return Objects.requireNonNull(result); 381 } 382 383 /** 384 * Create an OAuth client for a specific module 385 */ 386 @Nonnull 387 public OAuth2ClientDetailsJson createOAuthClient( 388 String theNodeId, String theModuleId, OAuth2WritableClientDetailsJson theClientDetails) { 389 Validate.notEmpty(theNodeId, "Node ID is required"); 390 Validate.notEmpty(theModuleId, "Module ID is required"); 391 Validate.notNull(theClientDetails, "Client details are required"); 392 393 OAuth2ClientDetailsJson result = myRestClient 394 .post() 395 .uri("openid-connect-clients/{nodeId}/{moduleId}", theNodeId, theModuleId) 396 .body(theClientDetails) 397 .retrieve() 398 .body(OAuth2ClientDetailsJson.class); 399 return Objects.requireNonNull(result); 400 } 401 402 /** 403 * Update an OAuth client for a specific module 404 */ 405 @Nonnull 406 public OAuth2ClientDetailsJson updateOAuthClient( 407 String theNodeId, 408 String theModuleId, 409 String theClientId, 410 OAuth2WritableClientDetailsJson theClientDetails) { 411 Validate.notEmpty(theNodeId, "Node ID is required"); 412 Validate.notEmpty(theModuleId, "Module ID is required"); 413 Validate.notEmpty(theClientId, "Client ID is required"); 414 Validate.notNull(theClientDetails, "Client details are required"); 415 416 OAuth2ClientDetailsJson result = myRestClient 417 .put() 418 .uri("openid-connect-clients/{nodeId}/{moduleId}/{clientId}", Map.of( 419 "nodeId", theNodeId, 420 "moduleId", theModuleId, 421 "clientId", theClientId)) 422 .body(theClientDetails) 423 .retrieve() 424 .body(OAuth2ClientDetailsJson.class); 425 return Objects.requireNonNull(result); 426 } 427 428 /** 429 * Delete an OAuth client 430 */ 431 public void deleteOAuthClient(String theNodeId, String theModuleId, String theClientId) { 432 Validate.notEmpty(theNodeId, "Node ID is required"); 433 Validate.notEmpty(theModuleId, "Module ID is required"); 434 Validate.notEmpty(theClientId, "Client ID is required"); 435 436 myRestClient 437 .delete() 438 .uri("openid-connect-clients/{nodeId}/{moduleId}/{clientId}", theNodeId, theModuleId, theClientId) 439 .retrieve() 440 .toBodilessEntity(); 441 } 442 443 /** 444 * Get an OAuth client 445 */ 446 @Nonnull 447 public OAuth2ClientDetailsJson getOAuthClient(String theNodeId, String theModuleId, String theClientId) { 448 Validate.notEmpty(theNodeId, "Node ID is required"); 449 Validate.notEmpty(theModuleId, "Module ID is required"); 450 Validate.notEmpty(theClientId, "Client ID is required"); 451 452 OAuth2ClientDetailsJson result = myRestClient 453 .get() 454 .uri("openid-connect-clients/{nodeId}/{moduleId}/{clientId}", theNodeId, theModuleId, theClientId) 455 .retrieve() 456 .body(OAuth2ClientDetailsJson.class); 457 return Objects.requireNonNull(result); 458 } 459 460 /** 461 * Create an OIDC server for a specific module 462 */ 463 @Nonnull 464 public JsonNode createOidcServer(String theNodeId, String theModuleId, JsonNode theServerDetails) { 465 Validate.notEmpty(theNodeId, "Node ID is required"); 466 Validate.notEmpty(theModuleId, "Module ID is required"); 467 Validate.notNull(theServerDetails, "Server details are required"); 468 469 JsonNode result = myRestClient 470 .post() 471 .uri("openid-connect-servers/{nodeId}/{moduleId}", theNodeId, theModuleId) 472 .body(theServerDetails) 473 .retrieve() 474 .body(JsonNode.class); 475 return Objects.requireNonNull(result); 476 } 477 478 /** 479 * Get an OIDC server 480 */ 481 @Nonnull 482 public JsonNode getOidcServer(String theNodeId, String theModuleId, String theServerId) { 483 Validate.notEmpty(theNodeId, "Node ID is required"); 484 Validate.notEmpty(theModuleId, "Module ID is required"); 485 Validate.notEmpty(theServerId, "Server ID is required"); 486 487 JsonNode result = myRestClient 488 .get() 489 .uri("openid-connect-servers/{nodeId}/{moduleId}/{serverId}", theNodeId, theModuleId, theServerId) 490 .retrieve() 491 .body(JsonNode.class); 492 return Objects.requireNonNull(result); 493 } 494 495 /** 496 * Update an OIDC server 497 */ 498 @Nonnull 499 public JsonNode updateOidcServer( 500 String theNodeId, String theModuleId, String theServerId, JsonNode theServerDetails) { 501 Validate.notEmpty(theNodeId, "Node ID is required"); 502 Validate.notEmpty(theModuleId, "Module ID is required"); 503 Validate.notEmpty(theServerId, "Server ID is required"); 504 Validate.notNull(theServerDetails, "Server details are required"); 505 506 JsonNode result = myRestClient 507 .put() 508 .uri("openid-connect-servers/{nodeId}/{moduleId}/{serverId}", theNodeId, theModuleId, theServerId) 509 .body(theServerDetails) 510 .retrieve() 511 .body(JsonNode.class); 512 return Objects.requireNonNull(result); 513 } 514 515 /** 516 * Get MDM links 517 */ 518 @Nonnull 519 public JsonNode getMdmLinks(String theModuleId) { 520 Validate.notEmpty(theModuleId, "Module ID is required"); 521 522 JsonNode result = myRestClient 523 .get() 524 .uri("mdm/{moduleId}/query-links", theModuleId) 525 .retrieve() 526 .body(JsonNode.class); 527 return Objects.requireNonNull(result); 528 } 529 530 /** 531 * Merge MDM golden resources 532 */ 533 @Nonnull 534 public JsonNode mergeMdmGoldenResources(String theModuleId, Object theRequest) { 535 Validate.notEmpty(theModuleId, "Module ID is required"); 536 Validate.notNull(theRequest, "Request is required"); 537 538 JsonNode result = myRestClient 539 .post() 540 .uri("mdm/{moduleId}/merge-golden-resources", theModuleId) 541 .body(theRequest) 542 .retrieve() 543 .body(JsonNode.class); 544 return Objects.requireNonNull(result); 545 } 546 547 /** 548 * Update MDM link 549 */ 550 @Nonnull 551 public JsonNode updateMdmLink(String theModuleId, Object theRequest) { 552 Validate.notEmpty(theModuleId, "Module ID is required"); 553 Validate.notNull(theRequest, "Request is required"); 554 555 JsonNode result = myRestClient 556 .post() 557 .uri("mdm/{moduleId}/update-link", theModuleId) 558 .body(theRequest) 559 .retrieve() 560 .body(JsonNode.class); 561 return Objects.requireNonNull(result); 562 } 563 564 /** 565 * Get MDM duplicate golden resources 566 */ 567 @Nonnull 568 public JsonNode getMdmDuplicateGoldenResources(String theModuleId) { 569 Validate.notEmpty(theModuleId, "Module ID is required"); 570 571 JsonNode result = myRestClient 572 .get() 573 .uri("mdm/{moduleId}/duplicate-golden-resources", theModuleId) 574 .retrieve() 575 .body(JsonNode.class); 576 return Objects.requireNonNull(result); 577 } 578 579 /** 580 * Submit an MDM operation to perform batch matching on all resources 581 * 582 * @param theModuleId The module ID 583 * @param theParameters The parameters for the MDM operation 584 * @return The response as a String 585 */ 586 @Nonnull 587 public String submitMdmOperation(String theModuleId, IBaseParameters theParameters) { 588 Validate.notEmpty(theModuleId, "Module ID is required"); 589 590 String body = toJsonString(theParameters); 591 592 String result = myRestClient 593 .post() 594 .uri("{moduleId}/mdm-submit", theModuleId) 595 .body(body) 596 .retrieve() 597 .body(String.class); 598 599 return result != null ? result : ""; 600 } 601 602 public static String toJsonString(IBaseResource theResource) { 603 FhirVersionEnum version = FhirVersionEnum.determineVersionForType(theResource.getClass()); 604 FhirContext ctx = FhirContext.forCached(version); 605 return ctx.newJsonParser().encodeResourceToString(theResource); 606 } 607 608 /** 609 * Exception thrown when a forbidden operation is attempted 610 */ 611 public static class ForbiddenOperationException extends RuntimeException { 612 public ForbiddenOperationException(String message, Throwable cause) { 613 super(message, cause); 614 } 615 } 616 617 /** 618 * Get the node configurations from the admin API. 619 * 620 * @return The node configurations 621 */ 622 @Nonnull 623 public NodeConfigurations getNodeConfigurations() { 624 NodeConfigurations result = myRestClient 625 .get() 626 .uri("/module-config/") 627 .retrieve() 628 .body(NodeConfigurations.class); 629 return Objects.requireNonNull(result); 630 } 631 632 /** 633 * Get the port number for a specific module from the node configurations. 634 * 635 * @param theModuleId The ID of the module to get the port for 636 * @return The port number, or the default FHIR port (8000) if not found 637 */ 638 public int getPortFromModule(String theModuleId) { 639 Validate.notEmpty(theModuleId, "Module ID is required"); 640 NodeConfigurations config = getNodeConfigurations(); 641 642 // Find the first node (assuming there's only one node in the container) 643 if (config.getNodes().isEmpty()) { 644 throw new IllegalStateException("No nodes found in node configuration"); 645 } else { 646 NodeConfigurations.NodeConfiguration node = config.getNodes().get(0); 647 648 // Find the module with the given ID 649 return node.getModule(theModuleId) 650 .map(module -> { 651 // Look for the port property in the module's configuration 652 for (NodeConfigurations.ModuleConfigProperty property : module.getConfigProperties()) { 653 if (property.getKey().equals("port")) { 654 try { 655 return Integer.parseInt(property.getValue()); 656 } catch (NumberFormatException e) { 657 // If the port is not a valid integer, fail. 658 throw new IllegalArgumentException("Invalid port number: " + property.getValue(), e); 659 } 660 } 661 } 662 // If no port property is found, return the default FHIR port 663 return null; 664 }) 665 .orElseThrow(() -> new IllegalArgumentException("Module with ID " + theModuleId + " not found in node configuration")); 666 } 667 668 } 669 670 /** 671 * Since many methods currently return JsonNode, if you have a known model you would like to bind to, you can convert a json object to it using this method 672 */ 673 public <T> T convertJsonNodeToModel(JsonNode jsonNode, Class<T> modelClass) throws JsonProcessingException { 674 // This is a hack to work around many of our return types being hidden in cdr-api instead of cdr-api-public. 675 // From Gary: We can't extract them cleanly yet to the fact that they often are relying on enums in clustermgr 676 // or enums in api that we do not currently want to make public, 677 // usually via 3 separate levels of nesting (e.g. node configurations -> module config -> ModuleTypeEnum 678 // TODO: Fix these, and move them to public, updating the method types to our model classes. 679 680 try { 681 ObjectMapper objectMapper = new ObjectMapper(); 682 // Direct conversion from JsonNode to model class without string conversion 683 return objectMapper.treeToValue(jsonNode, modelClass); 684 } catch (JsonProcessingException e) { 685 // Log the error with the actual JSON content for debugging 686 ourLog.error("Failed to convert JsonNode to " + modelClass.getSimpleName() + 687 ". JSON content: " + jsonNode.toString(), e); 688 throw e; 689 } 690 } 691 692 public <T> JsonNode convertModelToJsonNode(T model) throws JsonProcessingException { 693 try { 694 ObjectMapper objectMapper = new ObjectMapper(); 695 // Direct conversion from model to JsonNode without going through string 696 return objectMapper.valueToTree(model); 697 } catch (Exception e) { 698 throw new JsonProcessingException("Error converting model to JsonNode") {} ; 699 } 700 } 701 702}