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}