001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2025 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.test.app.harness.impl;
011
012import ca.cdr.test.app.clients.HL7V2RestClient;
013import ca.cdr.test.app.clients.AdminJsonRestClient;
014import ca.cdr.test.app.clients.NpmPackageClient;
015import ca.cdr.test.app.clients.OutboundSmartClient;
016import ca.cdr.test.app.clients.common.RequestFactoryUtil;
017import ca.cdr.test.app.harness.api.HarnessContext;
018import ca.cdr.test.app.harness.api.SmileHarness;
019import ca.cdr.test.model.NodeConfigurations;
020import ca.uhn.fhir.context.FhirContext;
021import ca.uhn.fhir.rest.client.api.IGenericClient;
022import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
023import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
024import org.apache.commons.lang3.Validate;
025import org.apache.commons.lang3.StringUtils;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028import org.springframework.web.client.RestClient;
029import org.springframework.cglib.core.Local;
030import org.springframework.web.client.RestClient;
031
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.List;
035import java.util.Map;
036import java.util.Optional;
037import java.util.Set;
038import java.util.function.Function;
039
040/**
041 * The {@code DockerSmileHarness} class is responsible for managing interaction
042 * with a Smile CDR environment running on the localhost. It provides utility methods to
043 * identify and interact with Smile modules, such as FHIR endpoints and HL7v2 endpoints,
044 * through exposed ports and administrative interfaces.
045 * <p>
046 * This class implements the {@link SmileHarness} interface to provide concrete
047 * implementations of the methods required to handle FHIR and administrative operations.
048 * <p>
049 * This class was generated partly with the help of Claude Sonnet 3.7
050 */
051public class LocalhostSmileHarness implements SmileHarness {
052        private static final Logger ourLog = LoggerFactory.getLogger(LocalhostSmileHarness.class);
053
054        Function<Integer, Integer> portResolver;
055        private final HarnessContext myContext;
056        private Map<String, Integer> myFhirModulePortMap = new HashMap<>();
057        private Map<String, Integer> myEndpointHl7V2PortMap = new HashMap<>();
058        private Map<String, Integer> mySmartEndpointPortMap = new HashMap<>();
059        private Map<String, Integer> myPackageRegistryPortMap = new HashMap<>();
060        private List<String> FHIR_COMPATIBLE_MODULE_TYPES = List.of(
061                "ENDPOINT_FHIR_REST",
062                "ENDPOINT_FHIR_REST_DSTU2",
063                "ENDPOINT_FHIR_REST_DSTU3",
064                "ENDPOINT_FHIR_REST_R4",
065                "ENDPOINT_FHIR_REST_R5",
066                "ENDPOINT_HYBRID_PROVIDERS",
067                "ENDPOINT_FHIR_GATEWAY"
068        );
069
070        private List<String> HL7V2_COMPATIBLE_MODULE_TYPES = List.of(
071                "ENDPOINT_HL7V2_IN_V2",
072                "ENDPOINT_HL7V2_IN"
073        );
074
075        private List<String> SMART_COMPATIBLE_MODULE_TYPES = List.of(
076                "SECURITY_OUT_SMART"
077        );
078
079        private List<String> PACKAGE_REGISTRY_COMPATIBLE_MODULE_TYPES = List.of(
080                "ENDPOINT_PACKAGE_REGISTRY"
081        );
082
083        public LocalhostSmileHarness(HarnessContext theContext) {
084                this(theContext, null);
085        }
086        public LocalhostSmileHarness(HarnessContext theContext, Map<Integer, Integer> theExposedPortMappings) {
087                myContext = theContext;
088                if (theExposedPortMappings == null || theExposedPortMappings.isEmpty()) {
089                        portResolver = (i) -> i;
090                } else {
091                        portResolver = theExposedPortMappings::get;
092                }
093                bootstrap();
094        }
095
096        private void bootstrap() {
097                AdminJsonRestClient adminJsonClient = bootstrapAdminJsonClient();
098
099                discoverFhirEndpoints(adminJsonClient);
100                discoverHL7V2Endpoints(adminJsonClient);
101                discoverSmartEndpoints(adminJsonClient);
102                discoverPackageRegistryEndpoints(adminJsonClient);
103        }
104
105        private void discoverHL7V2Endpoints(AdminJsonRestClient adminJsonClient) {
106                adminJsonClient.getNodeConfigurations().getNodes().forEach(node -> {
107                        node.getModules().forEach(module -> {
108                                if (HL7V2_COMPATIBLE_MODULE_TYPES.contains(module.getModuleType())) {
109                                        String port = module.getConfigProperty("port");
110                                        if (StringUtils.isNotBlank(port)) {
111                                                ourLog.info("Found HL7V2 Listening module. [moduleId={}, moduleId={}, port={}]", module.getModuleId(), module.getModuleType(), port);
112                                                myEndpointHl7V2PortMap.put(module.getModuleId(), Integer.parseInt(port));
113                                        } else {
114                                                ourLog.warn("Found FHIR-compatible module, but it had not port assigned! [moduleId={}, moduleId={}, port={}]", module.getModuleId(), module.getModuleType(), port);
115                                        }
116                                }
117                        });
118                });
119
120                validatePortResolverCanResolvePorts(myEndpointHl7V2PortMap);
121        }
122
123        private int resolvePort(int thePort) {
124                return portResolver.apply(thePort);
125        }
126
127        private void discoverFhirEndpoints(AdminJsonRestClient adminJsonClient) {
128                adminJsonClient.getNodeConfigurations().getNodes().forEach(node -> {
129                        node.getModules().forEach(module -> {
130                                if (FHIR_COMPATIBLE_MODULE_TYPES.contains(module.getModuleType())) {
131                                        String port = module.getConfigProperty("port");
132                                        if (StringUtils.isNotBlank(port)) {
133                                                ourLog.info("Found FHIR-compatible module. [moduleId={}, moduleId={}, port={}]", module.getModuleId(), module.getModuleType(), port);
134                                                myFhirModulePortMap.put(module.getModuleId(), Integer.parseInt(port));
135                                        } else {
136                                                ourLog.warn("Found FHIR-compatible module, but it had no port assigned! [moduleId={}, moduleId={}, port={}]", module.getModuleId(), module.getModuleType(), port);
137                                        }
138                                }
139                        });
140                });
141
142                validatePortResolverCanResolvePorts(myFhirModulePortMap);
143        }
144        private void validatePortResolverCanResolvePorts(Map<String, Integer> theModuleToPortMap) {
145                //Ensure all the endpoints we found have Mappings
146                theModuleToPortMap.forEach((key, value) -> {
147                        if (portResolver.apply(value) == null) {
148                                throw new IllegalStateException("Could not find a mapped port for module. Please ensure your port resolver can resolve this port! : " + key + " (" + value + ")");
149                        }
150                });
151        }
152
153        private void discoverSmartEndpoints(AdminJsonRestClient adminJsonClient) {
154                adminJsonClient.getNodeConfigurations().getNodes().forEach(node -> {
155                        node.getModules().forEach(module -> {
156                                if (SMART_COMPATIBLE_MODULE_TYPES.contains(module.getModuleType())) {
157                                        String port = module.getConfigProperty("port");
158                                        if (StringUtils.isNotBlank(port)) {
159                                                ourLog.info("Found SMART-compatible module. [moduleId={}, moduleType={}, port={}]", module.getModuleId(), module.getModuleType(), port);
160                                                mySmartEndpointPortMap.put(module.getModuleId(), Integer.parseInt(port));
161                                        } else {
162                                                ourLog.warn("Found SMART-compatible module, but it had no port assigned! [moduleId={}, moduleType={}, port={}]", module.getModuleId(), module.getModuleType(), port);
163                                        }
164                                }
165                        });
166                });
167                validatePortResolverCanResolvePorts(mySmartEndpointPortMap);
168        }
169
170        private void discoverPackageRegistryEndpoints(AdminJsonRestClient adminJsonClient) {
171                adminJsonClient.getNodeConfigurations().getNodes().forEach(node -> {
172                        node.getModules().forEach(module -> {
173                                if (PACKAGE_REGISTRY_COMPATIBLE_MODULE_TYPES.contains(module.getModuleType())) {
174                                        String port = module.getConfigProperty("port");
175                                        if (StringUtils.isNotBlank(port)) {
176                                                ourLog.info("Found Package Registry-compatible module. [moduleId={}, moduleType={}, port={}]", module.getModuleId(), module.getModuleType(), port);
177                                                myPackageRegistryPortMap.put(module.getModuleId(), Integer.parseInt(port));
178                                        } else {
179                                                ourLog.warn("Found Package Registry-compatible module, but it had no port assigned! [moduleId={}, moduleType={}, port={}]", module.getModuleId(), module.getModuleType(), port);
180                                        }
181                                }
182                        });
183                });
184                validatePortResolverCanResolvePorts(myPackageRegistryPortMap);
185        }
186
187        private AdminJsonRestClient bootstrapAdminJsonClient() {
188                Integer jsonAdminPort = myContext.jsonAdminPort();
189                AdminJsonRestClient adminJsonClient = null;
190                if (jsonAdminPort == null) {
191                        ourLog.warn("No JSON Admin port was provided in the provided harness context. Attempting to fallback to the default port [port=9000]");
192                        jsonAdminPort = 9000;
193                }  else {
194                        ourLog.info("Found a JSON Admin port in the context provided. [port={}]", jsonAdminPort);
195                }
196                adminJsonClient = getAdminJsonClient(jsonAdminPort);
197                try {
198                        ourLog.info("Performing a connectivity check for Admin JSON module. [host={}, port={}]", myContext.getContextRoot(), jsonAdminPort);
199                        adminJsonClient.getNodeConfigurations();
200                        ourLog.info("Connectivity check succeeded, client can be used! [host={}, port={}]", myContext.getContextRoot(), jsonAdminPort);
201                } catch(Exception e) {
202                        throw new InternalErrorException(String.format("Connectivity check failed for Admin JSON module. [host=%s, port=%d]", myContext.getContextRoot(), jsonAdminPort), e);
203                }
204                return adminJsonClient;
205        }
206
207        @Override
208        public IGenericClient getSuperuserFhirClient() {
209                IGenericClient fhirClient = getFhirClient();
210                fhirClient.registerInterceptor(new BasicAuthInterceptor(myContext.username(), myContext.password()));
211                return fhirClient;
212        }
213
214        @Override
215        public IGenericClient getSuperuserFhirClient(int thePort) {
216                IGenericClient fhirClient = getFhirClient(thePort);
217                fhirClient.registerInterceptor(new BasicAuthInterceptor(myContext.username(), myContext.password()));
218                return fhirClient;
219        }
220
221        @Override
222        public AdminJsonRestClient getAdminJsonClient() {
223                Integer jsonAdminPort = myContext.jsonAdminPort();
224                if (jsonAdminPort == null) {
225                        throw new IllegalStateException("JSON Admin port must be provided in the HarnessContext.");
226                }
227                return getAdminJsonClient(jsonAdminPort);
228        }
229
230        @Override
231        public AdminJsonRestClient getAdminJsonClient(int theAdminPort) {
232                Integer resolvedPort = portResolver.apply(theAdminPort);
233                String url = myContext.getContextRoot() + ":" + resolvedPort;
234                return AdminJsonRestClient.build(url, myContext.username(), myContext.password());
235        }
236
237        @Override
238        public IGenericClient getSuperuserFhirClient(String theModuleId) {
239                IGenericClient fhirClient = getFhirClient(theModuleId);
240                fhirClient.registerInterceptor(new BasicAuthInterceptor(myContext.username(), myContext.password()));
241                return fhirClient;
242        }
243
244        @Override
245        public IGenericClient getFhirClient() {
246                Optional<Integer> first= myFhirModulePortMap.values().stream().findFirst();
247                if (first.isEmpty()) {
248                        throw new IllegalStateException("No FHIR Endpoint found in the configuration.");
249                }
250                return getFhirClient(first.get());
251        }
252
253        @Override
254        public IGenericClient getFhirClient(int thePort) {
255                Integer resolvedPort = portResolver.apply(thePort);
256                return FhirContext.forR4().newRestfulGenericClient(myContext.protocol() +"://" + myContext.baseUrl() + ":" + resolvedPort);
257        }
258
259        @Override
260        public IGenericClient getFhirClient(String theModuleId) {
261                int port = getAdminJsonClient().getPortFromModule(theModuleId);
262                return getFhirClient(port);
263        }
264
265        /**
266         * Gets the only persistence module's FhirContext.
267         * If More or less than 1 persistence modules are present, throws {@link IllegalStateException}.}
268         * <p>
269         * See {@link #getFhirContext(String)} if you have multiple persistence modules.
270         *
271         * @return the {@link FhirContext} of the only persistence module.}
272         */
273        @Override
274        public FhirContext getFhirContext() {
275                NodeConfigurations nodeConfigs = getAdminJsonClient().getNodeConfigurations();
276                Set<String> persistenceModuleIds = new HashSet<>();
277
278                for (NodeConfigurations.NodeConfiguration node : nodeConfigs.getNodes()) {
279                        for (NodeConfigurations.ModuleConfiguration module : node.getModules()) {
280                                String moduleType = module.getModuleType();
281                                if (moduleType != null && moduleType.startsWith("PERSISTENCE_")) {
282                                        persistenceModuleIds.add(module.getModuleId());
283                                }
284                        }
285                }
286
287                if (persistenceModuleIds.size() > 1) {
288                        throw new IllegalStateException("Multiple different persistence modules found in the configuration, please specify. " + String.join(", ", persistenceModuleIds));
289                } else {
290                        return persistenceModuleIds.stream().map(this::getFhirContext).findFirst().orElseThrow(() -> new IllegalStateException("No persistence module found in the configuration."));
291                }
292        }
293
294        /**
295         * Get the FHIR context for a specific persistence  module.
296         *
297         * @param moduleId The ID of the persistence module to get the FHIR context for
298         * @return The FHIR context for the specified module
299         * @throws IllegalStateException if the module is not found or has an unsupported type
300         */
301        @Override
302        public FhirContext getFhirContext(String moduleId) {
303                Validate.notEmpty(moduleId, "Module ID is required");
304
305                NodeConfigurations nodeConfigs = getAdminJsonClient().getNodeConfigurations();
306
307                for (NodeConfigurations.NodeConfiguration node : nodeConfigs.getNodes()) {
308                        Optional<NodeConfigurations.ModuleConfiguration> moduleOpt = node.getModule(moduleId);
309                        if (moduleOpt.isPresent()) {
310                                NodeConfigurations.ModuleConfiguration module = moduleOpt.get();
311                                String moduleType = module.getModuleType();
312
313                                if (moduleType != null) {
314                                        if (moduleType.contains("R4")) {
315                                                return FhirContext.forR4();
316                                        } else if (moduleType.contains("DSTU3")) {
317                                                return FhirContext.forDstu3();
318                                        } else if (moduleType.contains("R5")) {
319                                                return FhirContext.forR5();
320                                        } else if (moduleType.contains("DSTU2")) {
321                                                return FhirContext.forDstu2();
322                                        }
323                                }
324
325                                throw new IllegalArgumentException("Could not retrieve a FhirContext from the provided module. [moduleId=%s, module_type=%s]".formatted(moduleId, moduleType));
326                        }
327                }
328                throw new IllegalArgumentException("No module found. [moduleId=%s]".formatted(moduleId));
329        }
330
331        @Override
332        public HL7V2RestClient getHL7V2RestClient() {
333                return getHL7V2RestClient(7000);
334        }
335
336        @Override
337        public HL7V2RestClient getHL7V2RestClient(int thePort) {
338                Integer resolvedPort = portResolver.apply(thePort);
339                return HL7V2RestClient.build(myContext.protocol() +"://" + myContext.baseUrl() + ":" + resolvedPort, myContext.username(), myContext.password());
340        }
341
342        @Override
343        public HL7V2RestClient getHL7V2RestClient(String theModuleId) {
344                int port = getAdminJsonClient().getPortFromModule(theModuleId);
345                Integer resolvedPort = portResolver.apply(port);
346                return getHL7V2RestClient(resolvedPort);
347        }
348
349        @Override
350        public OutboundSmartClient getOutboundSmartClient() {
351                // Look for the first available SMART module
352                Optional<Integer> firstSmartPort = mySmartEndpointPortMap.values().stream().findFirst();
353                if (firstSmartPort.isPresent()) {
354                        return getOutboundSmartClient(firstSmartPort.get());
355                } else {
356                        throw new IllegalStateException("No SMART endpoint found in the configuration for OutboundSmartClient.");
357                }
358        }
359
360        @Override
361        public OutboundSmartClient getOutboundSmartClient(int thePort) {
362                Integer resolvedPort = portResolver.apply(thePort);
363                String baseUrl = myContext.protocol() + "://" + myContext.baseUrl() + ":" + resolvedPort;
364                RestClient restClient = RestClient.builder().requestFactory(RequestFactoryUtil.buildSmileRequestFactory()).baseUrl(baseUrl).build();
365                return new OutboundSmartClient(restClient, baseUrl);
366        }
367
368        @Override
369        public OutboundSmartClient getOutboundSmartClient(String theModuleId) {
370                int port = getAdminJsonClient().getPortFromModule(theModuleId);
371                return getOutboundSmartClient(port);
372        }
373
374        @Override
375        public NpmPackageClient getNpmPackageClient() {
376                // Look for the first available Package Registry module
377                Optional<Integer> firstPackageRegistryPort = myPackageRegistryPortMap.values().stream().findFirst();
378                if (firstPackageRegistryPort.isPresent()) {
379                        return getNpmPackageClient(firstPackageRegistryPort.get());
380                } else {
381                        // Fall back to default port 8002 if no modules discovered
382                        ourLog.warn("No Package Registry endpoint found in configuration, using default port 8002");
383                        return getNpmPackageClient(8002);
384                }
385        }
386
387        @Override
388        public NpmPackageClient getNpmPackageClient(int thePort) {
389                Integer resolvedPort = portResolver.apply(thePort);
390                String baseUrl = myContext.protocol() + "://" + myContext.baseUrl() + ":" + resolvedPort;
391                return NpmPackageClient.build(baseUrl, myContext.username(), myContext.password());
392        }
393
394        @Override
395        public NpmPackageClient getNpmPackageClient(String theModuleId) {
396                int port = getAdminJsonClient().getPortFromModule(theModuleId);
397                return getNpmPackageClient(port);
398        }
399}