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}