From 500116690b4030555930f1ab8152cfef50044a0a Mon Sep 17 00:00:00 2001 From: DavidCroftDKFZ <46788708+DavidCroftDKFZ@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:28:42 +0200 Subject: [PATCH] Split functionality from the Sync class out into smaller classes Also corrected default Directory URL --- README.md | 28 +- pom.xml | 2 +- .../model/DirectoryCollectionPut.java | 5 +- .../directory_sync_service/fhir/FhirApi.java | 1 - .../model/StarModelData.java | 4 +- .../sync/BiobanksUpdater.java | 123 +++++++ .../sync/CollectionUpdater.java | 99 ++++++ .../sync/DiagnosisCorrections.java | 72 ++++ .../sync/StarModelUpdater.java | 89 +++++ .../directory_sync_service/sync/Sync.java | 315 +----------------- src/main/resources/application.yml | 2 +- 11 files changed, 416 insertions(+), 324 deletions(-) create mode 100644 src/main/java/de/samply/directory_sync_service/sync/BiobanksUpdater.java create mode 100644 src/main/java/de/samply/directory_sync_service/sync/CollectionUpdater.java create mode 100644 src/main/java/de/samply/directory_sync_service/sync/DiagnosisCorrections.java create mode 100644 src/main/java/de/samply/directory_sync_service/sync/StarModelUpdater.java diff --git a/README.md b/README.md index 01344da..d32dcc5 100644 --- a/README.md +++ b/README.md @@ -43,20 +43,20 @@ We recommend using Docker for running Directory sync. First, you will need to set up the environment variables for this: -| Variable | Purpose |Default if not specified | -|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------| -| DS_DIRECTORY_URL | Base URL of the Directory |https://directory.bbmri-eric.eu | -| DS_DIRECTORY_USER_NAME | User name for logging in to Directory | | -| DS_DIRECTORY_USER_PASS | Password for logging in to Directory | | -| DS_DIRECTORY_DEFAULT_COLLECTION_ID | ID of collection to be used if not in samples | | -| DS_DIRECTORY_MIN_DONORS | Minimum number of donors per star model hypercube |10 | -| DS_DIRECTORY_MAX_FACTS | Max number of star model hypercubes to be generated | | -| DS_DIRECTORY_ALLOW_STAR_MODEL | Set to 'True' to send star model info to Directory |False | -| DS_DIRECTORY_MOCK | Set to 'True' mock a Directory. In this mode, directory-sync will not contact the Directory. All Directory-related methods will simply return plausible fake values. |False | -| DS_FHIR_STORE_URL | URL for FHIR store |http://bridgehead-bbmri-blaze:8080| -| DS_TIMER_CRON | Execution interval for Directory sync, cron format | | -| DS_RETRY_MAX | Maximum number of retries when sync fails |10 | -| DS_RETRY_INTERVAL | Interval between retries (seconds) |20 seconds | +| Variable | Purpose | Default if not specified | +|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------| +| DS_DIRECTORY_URL | Base URL of the Directory | https://directory-backend.molgenis.net | +| DS_DIRECTORY_USER_NAME | User name for logging in to Directory | | +| DS_DIRECTORY_USER_PASS | Password for logging in to Directory | | +| DS_DIRECTORY_DEFAULT_COLLECTION_ID | ID of collection to be used if not in samples | | +| DS_DIRECTORY_MIN_DONORS | Minimum number of donors per star model hypercube | 10 | +| DS_DIRECTORY_MAX_FACTS | Max number of star model hypercubes to be generated | | +| DS_DIRECTORY_ALLOW_STAR_MODEL | Set to 'True' to send star model info to Directory | False | +| DS_DIRECTORY_MOCK | Set to 'True' mock a Directory. In this mode, directory-sync will not contact the Directory. All Directory-related methods will simply return plausible fake values. | False | +| DS_FHIR_STORE_URL | URL for FHIR store | http://bridgehead-bbmri-blaze:8080 | +| DS_TIMER_CRON | Execution interval for Directory sync, cron format | | +| DS_RETRY_MAX | Maximum number of retries when sync fails | 10 | +| DS_RETRY_INTERVAL | Interval between retries (seconds) | 20 seconds | DS_DIRECTORY_USER_NAME and DS_DIRECTORY_USER_PASS are mandatory. If you do not specify these, Directory sync will not run. Contact [Directory admin](directory@helpdesk.bbmri-eric.eu) to get login credentials. diff --git a/pom.xml b/pom.xml index 0a32912..db640b1 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ de.samply directory_sync_service - 1.4.4 + 1.4.5 directory_sync_service Directory sync https://github.com/samply/directory_sync_service diff --git a/src/main/java/de/samply/directory_sync_service/directory/model/DirectoryCollectionPut.java b/src/main/java/de/samply/directory_sync_service/directory/model/DirectoryCollectionPut.java index 186dcc5..5af09d2 100644 --- a/src/main/java/de/samply/directory_sync_service/directory/model/DirectoryCollectionPut.java +++ b/src/main/java/de/samply/directory_sync_service/directory/model/DirectoryCollectionPut.java @@ -364,9 +364,12 @@ public List getDiagnosisAvailable() { * * @param correctedDiagnoses A map containing diagnosis corrections, where the keys * represent the original diagnoses and the values represent - * the corrected diagnoses. + * the corrected diagnoses. Return without applying corrections + * if this parameter is null. */ public void applyDiagnosisCorrections(Map correctedDiagnoses) { + if (correctedDiagnoses == null) + return; for (Entity entity: getEntities()) { List directoryDiagnoses = entity.getDiagnosisAvailable().stream() .filter(diagnosis -> diagnosis != null && correctedDiagnoses.containsKey(diagnosis) && correctedDiagnoses.get(diagnosis) != null) diff --git a/src/main/java/de/samply/directory_sync_service/fhir/FhirApi.java b/src/main/java/de/samply/directory_sync_service/fhir/FhirApi.java index 8236518..a858e56 100644 --- a/src/main/java/de/samply/directory_sync_service/fhir/FhirApi.java +++ b/src/main/java/de/samply/directory_sync_service/fhir/FhirApi.java @@ -31,7 +31,6 @@ import java.util.function.Function; import java.util.HashSet; -import de.samply.directory_sync_service.model.StarModelData; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; diff --git a/src/main/java/de/samply/directory_sync_service/model/StarModelData.java b/src/main/java/de/samply/directory_sync_service/model/StarModelData.java index 9df5c90..49f4cbc 100644 --- a/src/main/java/de/samply/directory_sync_service/model/StarModelData.java +++ b/src/main/java/de/samply/directory_sync_service/model/StarModelData.java @@ -272,10 +272,12 @@ public int getFactCount() { *

* Note: This method directly modifies the factTables in-place. * - * @param diagnoses Maps FHIR diagnoses onto Directory diagnoses. + * @param diagnoses Maps FHIR diagnoses onto Directory diagnoses. If null, no corrections are applied. * @throws NullPointerException if diagnoses or any fact in factTables is null. */ public void applyDiagnosisCorrections(Map diagnoses) { + if (diagnoses == null) + return; for (Map fact: factTables) { if (!fact.containsKey("disease")) continue; diff --git a/src/main/java/de/samply/directory_sync_service/sync/BiobanksUpdater.java b/src/main/java/de/samply/directory_sync_service/sync/BiobanksUpdater.java new file mode 100644 index 0000000..53653a9 --- /dev/null +++ b/src/main/java/de/samply/directory_sync_service/sync/BiobanksUpdater.java @@ -0,0 +1,123 @@ +package de.samply.directory_sync_service.sync; + +import de.samply.directory_sync_service.Util; +import de.samply.directory_sync_service.directory.DirectoryApi; +import de.samply.directory_sync_service.directory.model.Biobank; +import de.samply.directory_sync_service.fhir.FhirApi; +import de.samply.directory_sync_service.model.BbmriEricId; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Organization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * Updates the biobanks in the local FHIR store with metadata from the Directory. + */ +public class BiobanksUpdater { + private static final Logger logger = LoggerFactory.getLogger(BiobanksUpdater.class); + public static final Function UPDATE_BIOBANK_NAME = t -> { + t.fhirBiobank.setName(t.dirBiobank.getName()); + return t; + }; + + /** + * Updates all biobanks from the FHIR server with information from the Directory. + * + * @return the individual {@link OperationOutcome}s from each update + */ + public static boolean updateBiobanksInFhirStore(FhirApi fhirApi, DirectoryApi directoryApi) { + // Retrieve the list of all biobanks + List organizations = fhirApi.listAllBiobanks(); + + // Check if the result is a failure or success + boolean succeeded = true; + if (organizations == null) { + logger.warn("error retrieving the biobanks"); + succeeded = false; + } else { + // If successful, process each biobank and update it on the FHIR server if necessary + for (Organization organization : organizations) { + // Update each biobank and report any errors + if (!updateBiobankInFhirStore(fhirApi, directoryApi, organization)) { + logger.warn("updateBiobankOnFhirServerIfNecessary: problem updating: " + organization.getIdElement().getValue()); + succeeded = false; + } + } + } + + return succeeded; + } + + /** + * Takes a biobank from FHIR and updates it with current information from the Directory. + * + * @param fhirBiobank the biobank to update. + * @return the {@link OperationOutcome} from the FHIR server update + */ + private static boolean updateBiobankInFhirStore(FhirApi fhirApi, DirectoryApi directoryApi, Organization fhirBiobank) { + logger.info("updateBiobankOnFhirServerIfNecessary: entered"); + + // Retrieve the biobank's BBMRI-ERIC identifier from the FHIR organization + Optional bbmriEricIdOpt = FhirApi.bbmriEricId(fhirBiobank); + + logger.info("updateBiobankOnFhirServerIfNecessary: bbmriEricIdOpt: " + bbmriEricIdOpt); + + // Check if the identifier is present, if not, return false + if (!bbmriEricIdOpt.isPresent()) { + logger.warn("updateBiobankOnFhirServerIfNecessary: Missing BBMRI-ERIC identifier"); + return false; + } + BbmriEricId bbmriEricId = bbmriEricIdOpt.get(); + + logger.info("updateBiobankOnFhirServerIfNecessary: bbmriEricId: " + bbmriEricId); + + // Fetch the corresponding biobank from the Directory API + Biobank directoryBiobank = directoryApi.fetchBiobank(bbmriEricId); + + logger.info("updateBiobankOnFhirServerIfNecessary: directoryBiobank: " + directoryBiobank); + + // Check if fetching the biobank was successful, if not, return false + if (directoryBiobank == null) { + logger.warn("updateBiobankOnFhirServerIfNecessary: Failed to fetch biobank from Directory API"); + return false; + } + + logger.info("updateBiobankOnFhirServerIfNecessary: Create a BiobankTuple containing the FHIR biobank and the Directory biobank"); + + // Create a BiobankTuple containing the FHIR biobank and the Directory biobank + BiobankTuple biobankTuple = new BiobankTuple(fhirBiobank, directoryBiobank); + + logger.info("updateBiobankOnFhirServerIfNecessary: Update the biobank name if necessary"); + + // Update the biobank name if necessary + BiobankTuple updatedBiobankTuple = UPDATE_BIOBANK_NAME.apply(biobankTuple); + + logger.info("updateBiobankOnFhirServerIfNecessary: Check if any changes have been made; if not, return a no-update necessary outcome"); + + // Check if any changes have been made; if not, return true (because this outcome is OK) + if (!updatedBiobankTuple.hasChanged()) { + logger.info("updateBiobankOnFhirServerIfNecessary: No update necessary"); + return true; + } + + logger.info("updateBiobankOnFhirServerIfNecessary: Update the biobank resource on the FHIR server if changes were made"); + + // Update the biobank resource on the FHIR server + OperationOutcome updateOutcome = fhirApi.updateResource(updatedBiobankTuple.fhirBiobank); + + String errorMessage = Util.getErrorMessageFromOperationOutcome(updateOutcome); + + if (!errorMessage.isEmpty()) { + logger.warn("updateBiobankOnFhirServerIfNecessary: Problem during FHIR store update"); + return false; + } + + logger.info("updateBiobankOnFhirServerIfNecessary: done!"); + + return true; + } +} diff --git a/src/main/java/de/samply/directory_sync_service/sync/CollectionUpdater.java b/src/main/java/de/samply/directory_sync_service/sync/CollectionUpdater.java new file mode 100644 index 0000000..3764338 --- /dev/null +++ b/src/main/java/de/samply/directory_sync_service/sync/CollectionUpdater.java @@ -0,0 +1,99 @@ +package de.samply.directory_sync_service.sync; + +import de.samply.directory_sync_service.Util; +import de.samply.directory_sync_service.converter.FhirCollectionToDirectoryCollectionPutConverter; +import de.samply.directory_sync_service.directory.DirectoryApi; +import de.samply.directory_sync_service.directory.MergeDirectoryCollectionGetToDirectoryCollectionPut; +import de.samply.directory_sync_service.directory.model.DirectoryCollectionGet; +import de.samply.directory_sync_service.directory.model.DirectoryCollectionPut; +import de.samply.directory_sync_service.fhir.FhirApi; +import de.samply.directory_sync_service.fhir.model.FhirCollection; +import de.samply.directory_sync_service.model.BbmriEricId; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * Update collections in the Directory with data from the local FHIR store. + */ +public class CollectionUpdater { + private static final Logger logger = LoggerFactory.getLogger(CollectionUpdater.class); + + /** + * Take information from the FHIR store and send aggregated updates to the Directory. + *

+ * This is a multi step process: + * 1. Fetch a list of collections objects from the FHIR store. These contain aggregated + * information over all specimens in the collections. + * 2. Convert the FHIR collection objects into Directory collection PUT DTOs. Copy + * over avaialble information from FHIR, converting where necessary. + * 3. Using the collection IDs found in the FHIR store, send queries to the Directory + * and fetch back the relevant GET collections. If any of the collection IDs cannot be + * found, this ie a breaking error. + * 4. Transfer data from the Directory GET collections to the corresponding Directory PUT + * collections. + * 5. Push the new information back to the Directory. + * + * @param defaultCollectionId The default collection ID to use for fetching collections from the FHIR store. + * @return A list of OperationOutcome objects indicating the outcome of the update operation. + */ + public static boolean sendUpdatesToDirectory(FhirApi fhirApi, DirectoryApi directoryApi, Map correctedDiagnoses, String defaultCollectionId) { + try { + BbmriEricId defaultBbmriEricCollectionId = BbmriEricId + .valueOf(defaultCollectionId) + .orElse(null); + + List fhirCollection = fhirApi.fetchFhirCollections(defaultBbmriEricCollectionId); + if (fhirCollection == null) { + logger.warn("Problem getting collections from FHIR store"); + return false; + } + logger.info("__________ sendUpdatesToDirectory: FHIR collection count): " + fhirCollection.size()); + + DirectoryCollectionPut directoryCollectionPut = FhirCollectionToDirectoryCollectionPutConverter.convert(fhirCollection); + if (directoryCollectionPut == null) { + logger.warn("Problem converting FHIR attributes to Directory attributes"); + return false; + } + logger.info("__________ sendUpdatesToDirectory: 1 directoryCollectionPut.getCollectionIds().size()): " + directoryCollectionPut.getCollectionIds().size()); + + List collectionIds = directoryCollectionPut.getCollectionIds(); + String countryCode = directoryCollectionPut.getCountryCode(); + directoryApi.relogin(); + DirectoryCollectionGet directoryCollectionGet = directoryApi.fetchCollectionGetOutcomes(countryCode, collectionIds); + if (directoryCollectionGet == null) { + logger.warn("Problem getting collections from Directory"); + return false; + } + logger.info("__________ sendUpdatesToDirectory: 1 directoryCollectionGet.getItems().size()): " + directoryCollectionGet.getItems().size()); + + if (!MergeDirectoryCollectionGetToDirectoryCollectionPut.merge(directoryCollectionGet, directoryCollectionPut)) { + logger.warn("Problem merging Directory GET attributes to Directory PUT attributes"); + return false; + } + logger.info("__________ sendUpdatesToDirectory: 2 directoryCollectionGet.getItems().size()): " + directoryCollectionGet.getItems().size()); + + // Apply corrections to ICD 10 diagnoses, to make them compatible with + // the Directory. + directoryCollectionPut.applyDiagnosisCorrections(correctedDiagnoses); + logger.info("__________ sendUpdatesToDirectory: 2 directoryCollectionPut.getCollectionIds().size()): " + directoryCollectionPut.getCollectionIds().size()); + + directoryApi.relogin(); + OperationOutcome updateOutcome = directoryApi.updateEntities(directoryCollectionPut); + String errorMessage = Util.getErrorMessageFromOperationOutcome(updateOutcome); + + if (!errorMessage.isEmpty()) { + logger.warn("sendUpdatesToDirectory: Problem during star model update"); + return false; + } + + return true; + } catch (Exception e) { + logger.warn("sendUpdatesToDirectory - unexpected error: " + Util.traceFromException(e)); + return false; + } + } +} diff --git a/src/main/java/de/samply/directory_sync_service/sync/DiagnosisCorrections.java b/src/main/java/de/samply/directory_sync_service/sync/DiagnosisCorrections.java new file mode 100644 index 0000000..6b3add2 --- /dev/null +++ b/src/main/java/de/samply/directory_sync_service/sync/DiagnosisCorrections.java @@ -0,0 +1,72 @@ +package de.samply.directory_sync_service.sync; + +import de.samply.directory_sync_service.Util; +import de.samply.directory_sync_service.converter.FhirToDirectoryAttributeConverter; +import de.samply.directory_sync_service.directory.DirectoryApi; +import de.samply.directory_sync_service.fhir.FhirApi; +import de.samply.directory_sync_service.model.BbmriEricId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DiagnosisCorrections { + private static final Logger logger = LoggerFactory.getLogger(DiagnosisCorrections.class); + + /** + * Generates corrections to the diagnoses obtained from the FHIR store, to make them + * compatible with the Directory. You should supply this method with an empty map + * via the correctedDiagnoses variable. This map will be filled by the method and + * you can subsequently use it elsewhere. + *

+ * This method performs the following steps: + *

+ * * Retrieves diagnoses from the FHIR store for specimens with identifiable collections and their associated patients. + * * Converts raw ICD-10 codes into MIRIAM-compatible codes. + * * Collects corrected diagnosis codes from the Directory API based on the MIRIAM-compatible codes. + * + * @param fhirApi + * @param directoryApi + * @param defaultCollectionId Default collection ID. May be null. + * @return A list containing diagnosis corrections. + * If any errors occur during the process, null is returned. + */ + public static Map generateDiagnosisCorrections(FhirApi fhirApi, DirectoryApi directoryApi, String defaultCollectionId) { + try { + Map correctedDiagnoses = new HashMap(); + // Convert string version of collection ID into a BBMRI ERIC ID. + BbmriEricId defaultBbmriEricCollectionId = BbmriEricId + .valueOf(defaultCollectionId) + .orElse(null); + + // Get all diagnoses from the FHIR store for specemins with identifiable + // collections and their associated patients. + List fhirDiagnoses = fhirApi.fetchDiagnoses(defaultBbmriEricCollectionId); + if (fhirDiagnoses == null) { + logger.warn("Problem getting diagnosis information from FHIR store"); + return null; + } + logger.info("__________ generateDiagnosisCorrections: fhirDiagnoses.size(): " + fhirDiagnoses.size()); + + // Convert the raw ICD 10 codes into MIRIAM-compatible codes and put the + // codes into a map with identical keys and values. + fhirDiagnoses.forEach(diagnosis -> { + String miriamDiagnosis = FhirToDirectoryAttributeConverter.convertDiagnosis(diagnosis); + correctedDiagnoses.put(miriamDiagnosis, miriamDiagnosis); + }); + logger.info("__________ generateDiagnosisCorrections: 1 correctedDiagnoses.size(): " + correctedDiagnoses.size()); + + // Get corrected diagnosis codes from the Directory + directoryApi.collectDiagnosisCorrections(correctedDiagnoses); + logger.info("__________ generateDiagnosisCorrections: 2 correctedDiagnoses.size(): " + correctedDiagnoses.size()); + + return correctedDiagnoses; + } catch (Exception e) { + logger.error("generateDiagnosisCorrections - unexpected error: " + Util.traceFromException(e)); + } + + return null; + } +} diff --git a/src/main/java/de/samply/directory_sync_service/sync/StarModelUpdater.java b/src/main/java/de/samply/directory_sync_service/sync/StarModelUpdater.java new file mode 100644 index 0000000..5d6e329 --- /dev/null +++ b/src/main/java/de/samply/directory_sync_service/sync/StarModelUpdater.java @@ -0,0 +1,89 @@ +package de.samply.directory_sync_service.sync; + +import de.samply.directory_sync_service.Util; +import de.samply.directory_sync_service.directory.CreateFactTablesFromStarModelInputData; +import de.samply.directory_sync_service.directory.DirectoryApi; +import de.samply.directory_sync_service.fhir.FhirApi; +import de.samply.directory_sync_service.fhir.PopulateStarModelInputData; +import de.samply.directory_sync_service.model.BbmriEricId; +import de.samply.directory_sync_service.model.StarModelData; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Update the star model data in the Directory with data from the local FHIR store. + */ +public class StarModelUpdater { + private static final Logger logger = LoggerFactory.getLogger(StarModelUpdater.class); + + /** + * Sends updates for Star Model data to the Directory service, based on FHIR store information. + * This method fetches Star Model input data from the FHIR store, generates star model fact tables, + * performs diagnosis corrections, and then updates the Directory service with the prepared data. + *

+ * The method handles errors by returning a list of OperationOutcome objects describing the issues. + *

+ * @param fhirApi + * @param directoryApi + * @param correctedDiagnoses + * @param defaultCollectionId The default BBMRI-ERIC collection ID for fetching data from the FHIR store. + * @param minDonors The minimum number of donors required for a fact to be included in the star model output. + * @param maxFacts The maximum number of facts to be included in the star model output. Negative number means no limit. + * @return A list of OperationOutcome objects indicating the outcome of the star model updates. + * + * @throws IllegalArgumentException if the defaultCollectionId is not a valid BbmriEricId. + */ + public static boolean sendStarModelUpdatesToDirectory(FhirApi fhirApi, DirectoryApi directoryApi, Map correctedDiagnoses, String defaultCollectionId, int minDonors, int maxFacts) { + logger.info("__________ sendStarModelUpdatesToDirectory: minDonors: " + minDonors); + try { + BbmriEricId defaultBbmriEricCollectionId = BbmriEricId + .valueOf(defaultCollectionId) + .orElse(null); + + // Pull data from the FHIR store and save it in a format suitable for generating + // star model hypercubes. + StarModelData starModelInputData = (new PopulateStarModelInputData(fhirApi)).populate(defaultBbmriEricCollectionId); + if (starModelInputData == null) { + logger.warn("Problem getting star model information from FHIR store"); + return false; + } + logger.info("__________ sendStarModelUpdatesToDirectory: number of collection IDs: " + starModelInputData.getInputCollectionIds().size()); + + directoryApi.relogin(); + + // Hypercubes containing less than the minimum number of donors will not be + // included in the star model output. + starModelInputData.setMinDonors(minDonors); + + // Take the patient list and the specimen list from starModelInputData and + // use them to generate the star model fact tables. + CreateFactTablesFromStarModelInputData.createFactTables(starModelInputData, maxFacts); + logger.info("__________ sendStarModelUpdatesToDirectory: 1 starModelInputData.getFactCount(): " + starModelInputData.getFactCount()); + + // Apply corrections to ICD 10 diagnoses, to make them compatible with + // the Directory. + starModelInputData.applyDiagnosisCorrections(correctedDiagnoses); + logger.info("__________ sendStarModelUpdatesToDirectory: 2 starModelInputData.getFactCount(): " + starModelInputData.getFactCount()); + + // Send fact tables to Direcory. + directoryApi.relogin(); + OperationOutcome updateOutcome = directoryApi.updateStarModel(starModelInputData); + String errorMessage = Util.getErrorMessageFromOperationOutcome(updateOutcome); + + if (!errorMessage.isEmpty()) { + logger.warn("sendStarModelUpdatesToDirectory: Problem during star model update"); + return false; + } + + return true; + } catch (Exception e) { + logger.warn("sendStarModelUpdatesToDirectory - unexpected error: " + Util.traceFromException(e)); + return false; + } + } +} diff --git a/src/main/java/de/samply/directory_sync_service/sync/Sync.java b/src/main/java/de/samply/directory_sync_service/sync/Sync.java index de745a9..9911c49 100644 --- a/src/main/java/de/samply/directory_sync_service/sync/Sync.java +++ b/src/main/java/de/samply/directory_sync_service/sync/Sync.java @@ -1,36 +1,14 @@ package de.samply.directory_sync_service.sync; -import de.samply.directory_sync_service.Util; -import de.samply.directory_sync_service.converter.FhirCollectionToDirectoryCollectionPutConverter; -import de.samply.directory_sync_service.directory.CreateFactTablesFromStarModelInputData; import de.samply.directory_sync_service.directory.DirectoryApi; -import de.samply.directory_sync_service.directory.MergeDirectoryCollectionGetToDirectoryCollectionPut; -import de.samply.directory_sync_service.fhir.PopulateStarModelInputData; -import de.samply.directory_sync_service.model.BbmriEricId; -import de.samply.directory_sync_service.directory.model.Biobank; -import de.samply.directory_sync_service.directory.model.DirectoryCollectionGet; -import de.samply.directory_sync_service.directory.model.DirectoryCollectionPut; import de.samply.directory_sync_service.fhir.FhirApi; -import de.samply.directory_sync_service.fhir.model.FhirCollection; -import de.samply.directory_sync_service.converter.FhirToDirectoryAttributeConverter; -import de.samply.directory_sync_service.model.StarModelData; import java.io.IOException; import java.util.Map; -import org.hl7.fhir.r4.model.OperationOutcome; -import org.hl7.fhir.r4.model.Organization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import static org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity.INFORMATION; - /** * Provides functionality to synchronize between a BBMRI Directory instance and a FHIR store in both directions. * This class provides methods to update biobanks, synchronize collection sizes, generate diagnosis corrections, @@ -63,13 +41,6 @@ public class Sync { private final int directoryMinDonors; private final int directoryMaxFacts; private final boolean directoryMock; - private Map correctedDiagnoses = null; - private FhirApi fhirApi; - private DirectoryApi directoryApi; - public static final Function UPDATE_BIOBANK_NAME = t -> { - t.fhirBiobank.setName(t.dirBiobank.getName()); - return t; - }; public Sync(String retryMax, String retryInterval, String fhirStoreUrl, String directoryUrl, String directoryUserName, String directoryUserPass, String directoryDefaultCollectionId, boolean directoryAllowStarModel, int directoryMinDonors, int directoryMaxFacts, boolean directoryMock) { this.retryMax = retryMax; @@ -106,26 +77,28 @@ public void syncWithDirectoryFailover() { } } - public boolean syncWithDirectory() { + private boolean syncWithDirectory() { + Map correctedDiagnoses = null; // Re-initialize helper classes every time this method gets called - fhirApi = new FhirApi(fhirStoreUrl); - directoryApi = new DirectoryApi(directoryUrl, directoryMock, directoryUserName, directoryUserPass); + FhirApi fhirApi = new FhirApi(fhirStoreUrl); + DirectoryApi directoryApi = new DirectoryApi(directoryUrl, directoryMock, directoryUserName, directoryUserPass); - if (!Util.reportOperationOutcomes(generateDiagnosisCorrections(directoryDefaultCollectionId))) { - logger.warn("syncWithDirectory: there was a problem during diagnosis corrections"); + correctedDiagnoses = DiagnosisCorrections.generateDiagnosisCorrections(fhirApi, directoryApi, directoryDefaultCollectionId); + if (correctedDiagnoses == null) { + logger.warn("syncWithDirectory: there was a problem during diagnosis corrections"); return false; } if (directoryAllowStarModel) - if (!Util.reportOperationOutcomes(sendStarModelUpdatesToDirectory(directoryDefaultCollectionId, directoryMinDonors, directoryMaxFacts))) { + if (!StarModelUpdater.sendStarModelUpdatesToDirectory(fhirApi, directoryApi, correctedDiagnoses, directoryDefaultCollectionId, directoryMinDonors, directoryMaxFacts)) { logger.warn("syncWithDirectory: there was a problem during star model update to Directory"); return false; } - if (!Util.reportOperationOutcomes(sendUpdatesToDirectory(directoryDefaultCollectionId))) { + if (!CollectionUpdater.sendUpdatesToDirectory(fhirApi, directoryApi, correctedDiagnoses, directoryDefaultCollectionId)) { logger.warn("syncWithDirectory: there was a problem during sync to Directory"); return false; } - if (!updateAllBiobanksOnFhirServerIfNecessary()) { + if (!BiobanksUpdater.updateBiobanksInFhirStore(fhirApi, directoryApi)) { logger.warn("syncWithDirectory: there was a problem during sync from Directory"); return false; } @@ -133,272 +106,4 @@ public boolean syncWithDirectory() { logger.info("__________ syncWithDirectory: all synchronization tasks finished"); return true; } - - /** - * Updates all biobanks from the FHIR server with information from the Directory. - * - * @return the individual {@link OperationOutcome}s from each update - */ - private boolean updateAllBiobanksOnFhirServerIfNecessary() { - // Retrieve the list of all biobanks - List organizations = fhirApi.listAllBiobanks(); - - // Check if the result is a failure or success - boolean succeeded = true; - if (organizations == null) { - logger.warn("error retrieving the biobanks"); - succeeded = false; - } else { - // If successful, process each biobank and update it on the FHIR server if necessary - for (Organization organization : organizations) { - // Update each biobank and report any errors - if (!updateBiobankOnFhirServerIfNecessary(organization)) { - logger.warn("updateBiobankOnFhirServerIfNecessary: problem updating: " + organization.getIdElement().getValue()); - succeeded = false; - } - } - } - - return succeeded; - } - - /** - * Takes a biobank from FHIR and updates it with current information from the Directory. - * - * @param fhirBiobank the biobank to update. - * @return the {@link OperationOutcome} from the FHIR server update - */ - private boolean updateBiobankOnFhirServerIfNecessary(Organization fhirBiobank) { - logger.info("updateBiobankOnFhirServerIfNecessary: entered"); - - // Retrieve the biobank's BBMRI-ERIC identifier from the FHIR organization - Optional bbmriEricIdOpt = FhirApi.bbmriEricId(fhirBiobank); - - logger.info("updateBiobankOnFhirServerIfNecessary: bbmriEricIdOpt: " + bbmriEricIdOpt); - - // Check if the identifier is present, if not, return false - if (!bbmriEricIdOpt.isPresent()) { - logger.warn("updateBiobankOnFhirServerIfNecessary: Missing BBMRI-ERIC identifier"); - return false; - } - BbmriEricId bbmriEricId = bbmriEricIdOpt.get(); - - logger.info("updateBiobankOnFhirServerIfNecessary: bbmriEricId: " + bbmriEricId); - - // Fetch the corresponding biobank from the Directory API - Biobank directoryBiobank = directoryApi.fetchBiobank(bbmriEricId); - - logger.info("updateBiobankOnFhirServerIfNecessary: directoryBiobank: " + directoryBiobank); - - // Check if fetching the biobank was successful, if not, return false - if (directoryBiobank == null) { - logger.warn("updateBiobankOnFhirServerIfNecessary: Failed to fetch biobank from Directory API"); - return false; - } - - logger.info("updateBiobankOnFhirServerIfNecessary: Create a BiobankTuple containing the FHIR biobank and the Directory biobank"); - - // Create a BiobankTuple containing the FHIR biobank and the Directory biobank - BiobankTuple biobankTuple = new BiobankTuple(fhirBiobank, directoryBiobank); - - logger.info("updateBiobankOnFhirServerIfNecessary: Update the biobank name if necessary"); - - // Update the biobank name if necessary - BiobankTuple updatedBiobankTuple = UPDATE_BIOBANK_NAME.apply(biobankTuple); - - logger.info("updateBiobankOnFhirServerIfNecessary: Check if any changes have been made; if not, return a no-update necessary outcome"); - - // Check if any changes have been made; if not, return true (because this outcome is OK) - if (!updatedBiobankTuple.hasChanged()) { - logger.info("updateBiobankOnFhirServerIfNecessary: No update necessary"); - return true; - } - - logger.info("updateBiobankOnFhirServerIfNecessary: Update the biobank resource on the FHIR server if changes were made"); - - // Update the biobank resource on the FHIR server - OperationOutcome updateOutcome = fhirApi.updateResource(updatedBiobankTuple.fhirBiobank); - - String errorMessage = Util.getErrorMessageFromOperationOutcome(updateOutcome); - - if (!errorMessage.isEmpty()) { - logger.warn("updateBiobankOnFhirServerIfNecessary: Problem during FHIR store update"); - return false; - } - - logger.info("updateBiobankOnFhirServerIfNecessary: done!"); - - return true; - } - - /** - * Generates corrections to the diagnoses obtained from the FHIR store, to make them - * compatible with the Directory. You should supply this method with an empty map - * via the correctedDiagnoses variable. This map will be filled by the method and - * you can subsequently use it elsewhere. - *

- * This method performs the following steps: - *

- * * Retrieves diagnoses from the FHIR store for specimens with identifiable collections and their associated patients. - * * Converts raw ICD-10 codes into MIRIAM-compatible codes. - * * Collects corrected diagnosis codes from the Directory API based on the MIRIAM-compatible codes. - * - * @param defaultCollectionId Default collection ID. May be null. - * @return A list containing a single OperationOutcome indicating the success of the diagnosis corrections process. - * If any errors occur during the process, an OperationOutcome with error details is returned. - */ - private List generateDiagnosisCorrections(String defaultCollectionId) { - correctedDiagnoses = new HashMap(); - try { - // Convert string version of collection ID into a BBMRI ERIC ID. - BbmriEricId defaultBbmriEricCollectionId = BbmriEricId - .valueOf(defaultCollectionId) - .orElse(null); - - // Get all diagnoses from the FHIR store for specemins with identifiable - // collections and their associated patients. - List fhirDiagnoses = fhirApi.fetchDiagnoses(defaultBbmriEricCollectionId); - if (fhirDiagnoses == null) { - logger.warn("Problem getting diagnosis information from FHIR store"); - } - logger.info("__________ generateDiagnosisCorrections: fhirDiagnoses.size(): " + fhirDiagnoses.size()); - - // Convert the raw ICD 10 codes into MIRIAM-compatible codes and put the - // codes into a map with identical keys and values. - fhirDiagnoses.forEach(diagnosis -> { - String miriamDiagnosis = FhirToDirectoryAttributeConverter.convertDiagnosis(diagnosis); - correctedDiagnoses.put(miriamDiagnosis, miriamDiagnosis); - }); - logger.info("__________ generateDiagnosisCorrections: 1 correctedDiagnoses.size(): " + correctedDiagnoses.size()); - - // Get corrected diagnosis codes from the Directory - directoryApi.collectDiagnosisCorrections(correctedDiagnoses); - logger.info("__________ generateDiagnosisCorrections: 2 correctedDiagnoses.size(): " + correctedDiagnoses.size()); - - // Return a successful outcome. - OperationOutcome outcome = new OperationOutcome(); - outcome.addIssue().setSeverity(INFORMATION).setDiagnostics("Diagnosis corrections generated successfully"); - return Collections.singletonList(outcome); - } catch (Exception e) { - return Util.createErrorOutcome("generateDiagnosisCorrections - unexpected error: " + Util.traceFromException(e)); - } - } - - /** - * Sends updates for Star Model data to the Directory service, based on FHIR store information. - * This method fetches Star Model input data from the FHIR store, generates star model fact tables, - * performs diagnosis corrections, and then updates the Directory service with the prepared data. - *

- * The method handles errors by returning a list of OperationOutcome objects describing the issues. - *

- * - * @param defaultCollectionId The default BBMRI-ERIC collection ID for fetching data from the FHIR store. - * @param minDonors The minimum number of donors required for a fact to be included in the star model output. - * @param maxFacts The maximum number of facts to be included in the star model output. Negative number means no limit. - * @return A list of OperationOutcome objects indicating the outcome of the star model updates. - * - * @throws IllegalArgumentException if the defaultCollectionId is not a valid BbmriEricId. - */ - private List sendStarModelUpdatesToDirectory(String defaultCollectionId, int minDonors, int maxFacts) { - logger.info("__________ sendStarModelUpdatesToDirectory: minDonors: " + minDonors); - try { - BbmriEricId defaultBbmriEricCollectionId = BbmriEricId - .valueOf(defaultCollectionId) - .orElse(null); - - // Pull data from the FHIR store and save it in a format suitable for generating - // star model hypercubes. - StarModelData starModelInputData = (new PopulateStarModelInputData(fhirApi)).populate(defaultBbmriEricCollectionId); - if (starModelInputData == null) - return Util.createErrorOutcome("Problem getting star model information from FHIR store"); - logger.info("__________ sendStarModelUpdatesToDirectory: number of collection IDs: " + starModelInputData.getInputCollectionIds().size()); - - directoryApi.relogin(); - - // Hypercubes containing less than the minimum number of donors will not be - // included in the star model output. - starModelInputData.setMinDonors(minDonors); - - // Take the patient list and the specimen list from starModelInputData and - // use them to generate the star model fact tables. - CreateFactTablesFromStarModelInputData.createFactTables(starModelInputData, maxFacts); - logger.info("__________ sendStarModelUpdatesToDirectory: 1 starModelInputData.getFactCount(): " + starModelInputData.getFactCount()); - - // Apply corrections to ICD 10 diagnoses, to make them compatible with - // the Directory. - if (correctedDiagnoses != null) - starModelInputData.applyDiagnosisCorrections(correctedDiagnoses); - logger.info("__________ sendStarModelUpdatesToDirectory: 2 starModelInputData.getFactCount(): " + starModelInputData.getFactCount()); - - // Send fact tables to Direcory. - directoryApi.relogin(); - List starModelUpdateOutcome = Collections.singletonList(directoryApi.updateStarModel(starModelInputData)); - logger.info("__________ sendStarModelUpdatesToDirectory: star model has been updated"); - // Return some kind of results count or whatever - return starModelUpdateOutcome; - } catch (Exception e) { - return Util.createErrorOutcome("sendStarModelUpdatesToDirectory - unexpected error: " + Util.traceFromException(e)); - } - } - - /** - * Take information from the FHIR store and send aggregated updates to the Directory. - *

- * This is a multi step process: - * 1. Fetch a list of collections objects from the FHIR store. These contain aggregated - * information over all specimens in the collections. - * 2. Convert the FHIR collection objects into Directory collection PUT DTOs. Copy - * over avaialble information from FHIR, converting where necessary. - * 3. Using the collection IDs found in the FHIR store, send queries to the Directory - * and fetch back the relevant GET collections. If any of the collection IDs cannot be - * found, this ie a breaking error. - * 4. Transfer data from the Directory GET collections to the corresponding Directory PUT - * collections. - * 5. Push the new information back to the Directory. - * - * @param defaultCollectionId The default collection ID to use for fetching collections from the FHIR store. - * @return A list of OperationOutcome objects indicating the outcome of the update operation. - */ - private List sendUpdatesToDirectory(String defaultCollectionId) { - try { - BbmriEricId defaultBbmriEricCollectionId = BbmriEricId - .valueOf(defaultCollectionId) - .orElse(null); - - List fhirCollection = fhirApi.fetchFhirCollections(defaultBbmriEricCollectionId); - if (fhirCollection == null) - return Util.createErrorOutcome("Problem getting collections from FHIR store"); - logger.info("__________ sendUpdatesToDirectory: FHIR collection count): " + fhirCollection.size()); - - DirectoryCollectionPut directoryCollectionPut = FhirCollectionToDirectoryCollectionPutConverter.convert(fhirCollection); - if (directoryCollectionPut == null) - return Util.createErrorOutcome("Problem converting FHIR attributes to Directory attributes"); - logger.info("__________ sendUpdatesToDirectory: 1 directoryCollectionPut.getCollectionIds().size()): " + directoryCollectionPut.getCollectionIds().size()); - - List collectionIds = directoryCollectionPut.getCollectionIds(); - String countryCode = directoryCollectionPut.getCountryCode(); - directoryApi.relogin(); - DirectoryCollectionGet directoryCollectionGet = directoryApi.fetchCollectionGetOutcomes(countryCode, collectionIds); - if (directoryCollectionGet == null) - return Util.createErrorOutcome("Problem getting collections from Directory"); - logger.info("__________ sendUpdatesToDirectory: 1 directoryCollectionGet.getItems().size()): " + directoryCollectionGet.getItems().size()); - - if (!MergeDirectoryCollectionGetToDirectoryCollectionPut.merge(directoryCollectionGet, directoryCollectionPut)) - return Util.createErrorOutcome("Problem merging Directory GET attributes to Directory PUT attributes"); - logger.info("__________ sendUpdatesToDirectory: 2 directoryCollectionGet.getItems().size()): " + directoryCollectionGet.getItems().size()); - - // Apply corrections to ICD 10 diagnoses, to make them compatible with - // the Directory. - if (correctedDiagnoses != null) - directoryCollectionPut.applyDiagnosisCorrections(correctedDiagnoses); - logger.info("__________ sendUpdatesToDirectory: 2 directoryCollectionPut.getCollectionIds().size()): " + directoryCollectionPut.getCollectionIds().size()); - - directoryApi.relogin(); - List outcomes = Collections.singletonList(directoryApi.updateEntities(directoryCollectionPut)); - logger.info("__________ sendUpdatesToDirectory: 2 outcomes: " + outcomes); - return outcomes; - } catch (Exception e) { - return Util.createErrorOutcome("sendUpdatesToDirectory - unexpected error: " + Util.traceFromException(e)); - } - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 715f3a0..b35d2e1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,7 +8,7 @@ ds: store: url: "http://bridgehead-bbmri-blaze:8080/fhir" directory: - url: "https://directory.bbmri-eric.eu" + url: "https://directory-backend.molgenis.net" user: name: "" pass: ""