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