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