From f2eeb55019ea02f392131f9865cfe019530e886a Mon Sep 17 00:00:00 2001 From: Lucas Triefenbach Date: Tue, 8 Jul 2025 15:45:54 +0200 Subject: [PATCH] Change Absolute File URLs To Forwarded URL --- README.md | 5 +-- .../torch/config/AppConfig.java | 8 ++++- .../torch/config/TorchProperties.java | 17 ---------- .../torch/rest/FhirController.java | 34 ++++++++++++++++--- .../torch/util/ResultFileManager.java | 32 ++++++++++------- .../torch/config/TestConfig.java | 7 +++- 6 files changed, 65 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 1cc244b0..9188f6c5 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,6 @@ For the latest build see: https://github.com/medizininformatik-initiative/torch/ | TORCH_CONCEPT_TREE_FILE | ontology/mapping_tree.json | The file for the concept tree mapping. | | TORCH_DSE_MAPPING_TREE_FILE | ontology/dse_mapping_tree.json | The file for DSE concept tree mapping. | | TORCH_USE_CQL | true | Flag indicating if CQL should be used. | -| TORCH_BASE_URL | – | The server name before the proxy from which torch is accessed | -| TORCH_OUTPUT_FILE_SERVER_URL | – | The URL to access Result location TORCH_RESULTS_DIR served by a proxy/fileserver | | LOG_LEVEL
_DE_MEDIZININFORMATIKINITIATIVE_TORCH | info | Log level for torch core functionality. | | LOG_LEVEL
_CA_UHN_FHIR | error | Log level for HAPI FHIR library. | | SPRING_PROFILES_ACTIVE | active | The active Spring profile. | @@ -217,6 +215,9 @@ This can be used to track the progress of your data extraction. If a server is set up for the files e.g. NGINX, the files can be fetched by a Request on the URL set in TORCH_OUTPUT_FILE_SERVER_URL in [enviroment variables](#environment-variables). +**Torch assumes that the files are served from the baseurl that is next to the fhir api** +E.g.: if localhost:8080/test/fhir is the url from which torch gets called (before forwarding), then +the file url would start with **http://localhost:8080/test/** ```sh curl -s "http://localhost:8080/da4a1c56-f5d9-468c-b57a-b8186ea4fea8/f33634bd-d51b-463c-a956-93409d96935f.ndjson" diff --git a/src/main/java/de/medizininformatikinitiative/torch/config/AppConfig.java b/src/main/java/de/medizininformatikinitiative/torch/config/AppConfig.java index 08f092a9..3b51bef1 100644 --- a/src/main/java/de/medizininformatikinitiative/torch/config/AppConfig.java +++ b/src/main/java/de/medizininformatikinitiative/torch/config/AppConfig.java @@ -58,6 +58,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; @@ -92,6 +93,11 @@ public AppConfig(TorchProperties torchProperties) { this.torchProperties = torchProperties; } + @Bean + public ForwardedHeaderTransformer forwardedHeaderTransformer() { + return new ForwardedHeaderTransformer(); + } + @Bean public String searchParametersFile(@Value("${torch.search_parameters_file}") String searchParametersFile) { @@ -370,7 +376,7 @@ public StructureDefinitionHandler cdsStructureDefinitionHandler(ResourceReader r @Bean public ResultFileManager resultFileManager(FhirContext fhirContext) { - return new ResultFileManager(torchProperties.results().dir(), torchProperties.results().persistence(), fhirContext, torchProperties.base().url(), torchProperties.output().file().server().url()); + return new ResultFileManager(torchProperties.results().dir(), torchProperties.results().persistence(), fhirContext); } @Bean diff --git a/src/main/java/de/medizininformatikinitiative/torch/config/TorchProperties.java b/src/main/java/de/medizininformatikinitiative/torch/config/TorchProperties.java index f1310b56..08391c4a 100644 --- a/src/main/java/de/medizininformatikinitiative/torch/config/TorchProperties.java +++ b/src/main/java/de/medizininformatikinitiative/torch/config/TorchProperties.java @@ -4,8 +4,6 @@ @ConfigurationProperties(prefix = "torch") public record TorchProperties( - Base base, - Output output, Profile profile, Mapping mapping, Fhir fhir, @@ -19,24 +17,9 @@ public record TorchProperties( String dseMappingTreeFile, boolean useCql ) { - - public record Base(String url) { - - } - public record Max(int connections) { } - public record Output(File file) { - public record File(Server server) { - public record Server(String url) { - - } - } - - } - - public record Profile(String dir) { } diff --git a/src/main/java/de/medizininformatikinitiative/torch/rest/FhirController.java b/src/main/java/de/medizininformatikinitiative/torch/rest/FhirController.java index 395323df..5b894866 100644 --- a/src/main/java/de/medizininformatikinitiative/torch/rest/FhirController.java +++ b/src/main/java/de/medizininformatikinitiative/torch/rest/FhirController.java @@ -20,10 +20,12 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import java.io.IOException; +import java.net.URI; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -47,6 +49,7 @@ public class FhirController { private static final Logger logger = LoggerFactory.getLogger(FhirController.class); private static final MediaType MEDIA_TYPE_FHIR_JSON = MediaType.valueOf("application/fhir+json"); + public static final String FHIR_STATUS = "/fhir/__status/"; private final ObjectMapper objectMapper; private final FhirContext fhirContext; @@ -82,7 +85,7 @@ private record DecodedContent(byte[] crtdl, List patientIds) { @Autowired public FhirController(ObjectMapper objectMapper, FhirContext fhirContext, ResultFileManager resultFileManager, - CrtdlProcessingService crtdlProcessingService, CrtdlValidatorService validatorService) { + CrtdlProcessingService crtdlProcessingService, CrtdlValidatorService validatorService) { this.objectMapper = objectMapper; this.fhirContext = fhirContext; this.resultFileManager = resultFileManager; @@ -119,7 +122,7 @@ private static DecodedContent decodeCrtdlContent(Parameters parameters) { public RouterFunction queryRouter() { logger.info("Init FhirController Router"); return route(POST("/fhir/$extract-data").and(accept(MEDIA_TYPE_FHIR_JSON)), this::handleExtractData) - .andRoute(GET("/fhir/__status/{jobId}"), this::checkStatus).andRoute(GET("/fhir/__status/"), this::getGlobalStatus); + .andRoute(GET("/fhir/__status/{jobId}"), this::checkStatus).andRoute(GET(FHIR_STATUS), this::getGlobalStatus); } private record DecodedCRTDLContent(Crtdl crtdl, List patientIds) { @@ -183,7 +186,7 @@ public Mono handleExtractData(ServerRequest request) { .subscribeOn(Schedulers.boundedElastic()).subscribe(); // final fire-and-forget return accepted() - .header("Content-Location", request.uriBuilder().replacePath("/fhir/__status/" + jobId).build().toString()) + .header("Content-Location", request.uriBuilder().replacePath(FHIR_STATUS + jobId).build().toString()) .build(); }) .onErrorResume(IllegalArgumentException.class, e -> { @@ -228,7 +231,30 @@ private Crtdl parseCrtdlContent(byte[] content) throws IOException { return objectMapper.readValue(content, Crtdl.class); } + public String stripToBasePath(URI originalUri, String basePath) { + + String fullPath = originalUri.getPath(); + + int index = fullPath.indexOf(basePath); + if (index == -1) { + return originalUri.toString(); // fallback: no change + } + + String newPath = fullPath.substring(0, index); + + return UriComponentsBuilder + .fromUri(originalUri) + .replacePath(newPath) + .replaceQuery(null) + .fragment(null) + .build() + .toUriString(); + } + + public Mono checkStatus(ServerRequest request) { + String truncatedUrl = stripToBasePath(request.uri(), FHIR_STATUS); + logger.info("Base url: {}", truncatedUrl); var jobId = request.pathVariable("jobId"); HttpStatus status = resultFileManager.getStatus(jobId); @@ -242,7 +268,7 @@ public Mono checkStatus(ServerRequest request) { case HttpStatus.OK -> { // Capture the full request URL and transaction time String transactionTime = DateTimeFormatter.ISO_INSTANT.format(Instant.now()); - return Mono.fromCallable(() -> resultFileManager.loadBundleFromFileSystem(jobId, transactionTime)) + return Mono.fromCallable(() -> resultFileManager.loadBundleFromFileSystem(truncatedUrl, jobId, transactionTime)) .flatMap(bundleMap -> { if (bundleMap == null) { return ServerResponse.notFound().build(); diff --git a/src/main/java/de/medizininformatikinitiative/torch/util/ResultFileManager.java b/src/main/java/de/medizininformatikinitiative/torch/util/ResultFileManager.java index c7a18295..f5dbb0be 100644 --- a/src/main/java/de/medizininformatikinitiative/torch/util/ResultFileManager.java +++ b/src/main/java/de/medizininformatikinitiative/torch/util/ResultFileManager.java @@ -4,17 +4,14 @@ import ca.uhn.fhir.context.FhirContext; import de.medizininformatikinitiative.torch.management.OperationOutcomeCreator; import de.medizininformatikinitiative.torch.model.consent.PatientBatchWithConsent; -import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.OperationOutcome; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import java.io.BufferedWriter; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -23,7 +20,13 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.Duration; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; @@ -38,18 +41,14 @@ public class ResultFileManager { private final Path resultsDirPath; private final FhirContext fhirContext; - private final String hostname; - private final String fileServerName; public final ConcurrentHashMap jobStatusMap = new ConcurrentHashMap<>(); - public ResultFileManager(String resultsDir, String duration, FhirContext fhirContext, String hostname, String fileServerName) { + public ResultFileManager(String resultsDir, String duration, FhirContext fhirContext) { this.resultsDirPath = Paths.get(resultsDir).toAbsolutePath(); this.fhirContext = fhirContext; Duration duration1 = Duration.parse(duration); - this.hostname = hostname; - this.fileServerName = fileServerName; logger.debug("Duration of persistence {}", duration1); @@ -213,7 +212,15 @@ public String loadErrorFromFileSystem(String jobId) { } } - public Map loadBundleFromFileSystem(String jobId, String transactionTime) { + /** + * Generates the server response for a successful operation. + * + * @param url of the calling request. + * @param jobId id to be handled + * @param transactionTime time of the transaction + * @return Map Containing the server response with file locations + */ + public Map loadBundleFromFileSystem(String url, String jobId, String transactionTime) { Map response = new HashMap<>(); try { Path jobDir = getJobDirectory(jobId); @@ -224,10 +231,9 @@ public Map loadBundleFromFileSystem(String jobId, String transac Files.list(jobDir).forEach(file -> { String fileName = file.getFileName().toString(); - String url = fileServerName + "/" + jobId + "/" + fileName; Map fileEntry = new HashMap<>(); - fileEntry.put("url", url); + fileEntry.put("url", url + "/" + jobId + "/" + fileName); if (fileName.endsWith(".ndjson")) { fileEntry.put("type", "NDJSON Bundle"); @@ -244,7 +250,7 @@ public Map loadBundleFromFileSystem(String jobId, String transac logger.debug("OutputFiles size {}", outputFiles.size()); response.put("transactionTime", transactionTime); - response.put("request", hostname + "/fhir/$extract-data"); + response.put("request", url + "/fhir/$extract-data"); response.put("requiresAccessToken", false); response.put("output", outputFiles); response.put("deleted", deletedFiles); diff --git a/src/test/java/de/medizininformatikinitiative/torch/config/TestConfig.java b/src/test/java/de/medizininformatikinitiative/torch/config/TestConfig.java index 539afc66..ac84eb71 100644 --- a/src/test/java/de/medizininformatikinitiative/torch/config/TestConfig.java +++ b/src/test/java/de/medizininformatikinitiative/torch/config/TestConfig.java @@ -52,6 +52,7 @@ import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; @@ -89,6 +90,10 @@ public TestConfig(TorchProperties torchProperties) { this.torchProperties = torchProperties; } + @Bean + public ForwardedHeaderTransformer forwardedHeaderTransformer() { + return new ForwardedHeaderTransformer(); + } @Bean public CascadingDelete cascadingDelete() { @@ -304,7 +309,7 @@ public StructureDefinitionHandler cdsStructureDefinitionHandler(ResourceReader r @Bean public ResultFileManager resultFileManager(FhirContext fhirContext) { - return new ResultFileManager(torchProperties.results().dir(), torchProperties.results().persistence(), fhirContext, torchProperties.base().url(), torchProperties.output().file().server().url()); + return new ResultFileManager(torchProperties.results().dir(), torchProperties.results().persistence(), fhirContext); } @Bean