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}