diff --git a/efsity/src/main/java/org/smartregister/command/TranslateCommand.java b/efsity/src/main/java/org/smartregister/command/TranslateCommand.java index 7f693fac..9a484654 100644 --- a/efsity/src/main/java/org/smartregister/command/TranslateCommand.java +++ b/efsity/src/main/java/org/smartregister/command/TranslateCommand.java @@ -8,6 +8,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; @@ -58,6 +59,9 @@ public class TranslateCommand implements Runnable { private static String url = "http://hl7.org/fhir/StructureDefinition/translation"; + Path tempsConfig = null; + Path tempFilePath = null; + @Override public void run() { if (!Arrays.asList(modes).contains(mode)) { @@ -76,27 +80,27 @@ public void run() { FctUtils.printInfo(String.format("Input file \u001b[35m%s\u001b[0m", resourceFile)); try { + Path translationsDirectoryPath = inputFilePath.getParent().resolve("translations"); + + if (Objects.equals(extractionType, "configs")) { + tempsConfig = Files.createTempDirectory("configs"); + } else tempsConfig = null; + if (!Files.exists(translationsDirectoryPath)) + Files.createDirectories(translationsDirectoryPath); + if (translationFile == null) { + translationFile = translationsDirectoryPath + "/strings_default.properties"; + } // Check if the input path is a directory or a JSON file if (Files.isDirectory(inputFilePath)) { - if (Objects.equals(extractionType, "configs") || inputFilePath.endsWith("configs")) { - extractionType = "configs"; Set targetFields = FCTConstants.configTranslatables; - - if (translationFile == null) { - translationFile = - inputFilePath.resolve("translations/strings_config.properties").toString(); - } + copyDirectoryContent(inputFilePath, tempsConfig); extractContent(translationFile, inputFilePath, targetFields, extractionType); } else if (Objects.equals(extractionType, "fhirContent") || inputFilePath.endsWith("fhir_content")) { extractionType = "fhirContent"; Set targetFields = FCTConstants.questionnaireTranslatables; - if (translationFile == null) { - translationFile = - inputFilePath.resolve("translations/strings_default.properties").toString(); - } if (!inputFilePath.endsWith("questionnaires")) { inputFilePath = inputFilePath.resolve("questionnaires"); } @@ -109,13 +113,7 @@ public void run() { if (Files.exists(configsPath) && Files.isDirectory(configsPath)) { extractionType = "configs"; Set targetFields = FCTConstants.configTranslatables; - String configsTranslationFile = null; - configsTranslationFile = - Objects.requireNonNullElseGet( - translationFile, - () -> - configsPath.resolve("translations/strings_config.properties").toString()); - extractContent(configsTranslationFile, configsPath, targetFields, extractionType); + extractContent(translationFile, configsPath, targetFields, extractionType); } else { FctUtils.printWarning("`configs` directory not found in directory"); } @@ -125,16 +123,8 @@ public void run() { && Files.isDirectory(questionnairePath)) { extractionType = "fhirContent"; Set targetFields = FCTConstants.questionnaireTranslatables; - String contentTranslationFile = null; - contentTranslationFile = - Objects.requireNonNullElseGet( - translationFile, - () -> - fhirContentPath - .resolve("translations/strings_default.properties") - .toString()); - extractContent( - contentTranslationFile, questionnairePath, targetFields, extractionType); + + extractContent(translationFile, questionnairePath, targetFields, extractionType); } else { FctUtils.printWarning( "`fhir_content` or `fhir_content/questionnaires` directory not found in directory"); @@ -349,16 +339,28 @@ private static JsonNode createExtensionNode(String locale, String translation) { return extensionArray; } - private static void extractContent( + private void extractContent( String translationFile, Path inputFilePath, Set targetFields, String extractionType) throws IOException, NoSuchAlgorithmException { Map textToHash = new HashMap<>(); Path propertiesFilePath = Paths.get(translationFile); - if (Files.isRegularFile(inputFilePath) && inputFilePath.toString().toLowerCase(Locale.ENGLISH).endsWith(".json")) { - if (Objects.equals(extractionType, "configs")) { + String configFileSubDirectory = + inputFilePath.subpath(2, inputFilePath.getNameCount() - 1).toString(); + try { + Path tempConfigSubDirectory = tempsConfig.resolve(configFileSubDirectory); + if (!Files.exists(tempConfigSubDirectory)) { + Files.createDirectories(tempConfigSubDirectory); + tempFilePath = tempConfigSubDirectory.resolve(inputFilePath.getFileName()); + // copy over content + Files.copy(inputFilePath, tempFilePath, StandardCopyOption.REPLACE_EXISTING); + } + + } catch (IOException e) { + throw new RuntimeException("Error creating temp file " + e); + } // Extract and replace target fields with hashed values ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = @@ -366,7 +368,9 @@ private static void extractContent( FctUtils.printInfo( String.format( "Extracting config file \u001b[35m%s\u001b[0m", inputFilePath.toString())); - replaceTargetFieldsWithHashedValues(rootNode, targetFields, textToHash, inputFilePath); + + replaceTargetFieldsWithHashedValues( + rootNode, targetFields, textToHash, inputFilePath, tempsConfig); } else { // For other types (content/questionnaire), extract as usual processJsonFile(inputFilePath, textToHash, targetFields); @@ -374,7 +378,8 @@ private static void extractContent( } else if (Files.isDirectory(inputFilePath)) { // Handle the case where inputFilePath is a directory (folders may contain multiple JSON // files) - Files.walk(inputFilePath) + + Files.walk(tempsConfig) .filter(Files::isRegularFile) .filter(file -> file.toString().endsWith(".json")) .forEach( @@ -389,25 +394,39 @@ private static void extractContent( FctUtils.printInfo( String.format( "Extracting config file \u001b[35m%s\u001b[0m", file.toString())); - replaceTargetFieldsWithHashedValues(rootNode, targetFields, textToHash, file); + + replaceTargetFieldsWithHashedValues( + rootNode, targetFields, textToHash, file, tempsConfig); } else { // For other types (content/questionnaire), extract as usual processJsonFile(file, textToHash, targetFields); } } catch (IOException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); + deleteDirectoryRecursively(tempsConfig); + throw new RuntimeException( + "Error while reading file " + file.getFileName() + " " + e); } }); } else { throw new RuntimeException("Provide a valid `resourceFile` directory or file."); } + // Copy over translations from temp + if (extractionType.equals("configs")) { + if (Files.isDirectory(inputFilePath)) copyDirectoryContent(tempsConfig, inputFilePath); + else { + assert tempFilePath != null; + Files.copy(tempFilePath, inputFilePath, StandardCopyOption.REPLACE_EXISTING); + } + } + Properties existingProperties = FctUtils.readPropertiesFile(propertiesFilePath.toString()); // Merge existing properties with new properties existingProperties.putAll(textToHash); writePropertiesFile(existingProperties, translationFile); FctUtils.printInfo(String.format("Translation file\u001b[36m %s \u001b[0m", translationFile)); + if (tempsConfig != null) deleteDirectoryRecursively(tempsConfig); } private static void processJsonFile( @@ -421,8 +440,12 @@ private static void processJsonFile( } private static void replaceTargetFieldsWithHashedValues( - JsonNode node, Set targetFields, Map textToHash, Path filePath) - throws NoSuchAlgorithmException { + JsonNode node, + Set targetFields, + Map textToHash, + Path filePath, + Path tempConfigsDir) + throws NoSuchAlgorithmException, IOException { if (node.isObject()) { ObjectNode objectNode = (ObjectNode) node; @@ -444,7 +467,8 @@ private static void replaceTargetFieldsWithHashedValues( JsonNode fieldValue = field.getValue(); if (fieldValue.isObject() || fieldValue.isArray()) { // Recursively update nested objects or arrays - replaceTargetFieldsWithHashedValues(fieldValue, targetFields, textToHash, filePath); + replaceTargetFieldsWithHashedValues( + fieldValue, targetFields, textToHash, filePath, tempConfigsDir); } } } else if (node.isArray()) { @@ -453,18 +477,20 @@ private static void replaceTargetFieldsWithHashedValues( JsonNode arrayElement = arrayNode.get(i); if (arrayElement.isObject() || arrayElement.isArray()) { // Recursively update nested objects or arrays - replaceTargetFieldsWithHashedValues(arrayElement, targetFields, textToHash, filePath); + replaceTargetFieldsWithHashedValues( + arrayElement, targetFields, textToHash, filePath, tempConfigsDir); } } } - // Write the updated JSON back to the file - try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) { + String configFileSubDirectory = filePath.subpath(2, filePath.getNameCount()).toString(); + + Path tempFilePath = tempConfigsDir.resolve(configFileSubDirectory); + // Write the updated JSON to temp file + try (BufferedWriter writer = Files.newBufferedWriter(tempFilePath, StandardCharsets.UTF_8)) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.writeValue(writer, node); - } catch (IOException e) { - throw new RuntimeException("Failed to write the updated JSON to file.", e); } } @@ -524,4 +550,63 @@ private static void writePropertiesFile(Properties properties, String filePath) properties.store(output, null); } } + + public void copyDirectoryContent(Path sourceDir, Path destinationDir) { + try { + Files.walkFileTree( + sourceDir, + new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + Path targetDir = destinationDir.resolve(sourceDir.relativize(dir)); + if (!Files.exists(targetDir)) { + Files.createDirectory(targetDir); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + + Files.copy( + file, + destinationDir.resolve(sourceDir.relativize(file)), + StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void deleteDirectoryRecursively(Path dirPath) { + + try { + // Delete the directory and its contents recursively + Files.walkFileTree( + dirPath, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + + System.out.println("Directory and its contents deleted successfully: " + dirPath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/efsity/src/test/java/org/smartregister/command/TranslateCommandTest.java b/efsity/src/test/java/org/smartregister/command/TranslateCommandTest.java index 4b1d290a..ac155990 100644 --- a/efsity/src/test/java/org/smartregister/command/TranslateCommandTest.java +++ b/efsity/src/test/java/org/smartregister/command/TranslateCommandTest.java @@ -13,6 +13,7 @@ import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.smartregister.util.FctUtils; public class TranslateCommandTest { @@ -67,6 +68,41 @@ public void testRunExtract() throws IOException { tempDefaultPropertiesPath.toFile().delete(); } + @Test + public void testRunExtractConfigWithCleanConfigsFolderRunsSuccessfully() throws IOException { + Path cleanConfigsFolder = Paths.get("src/test/resources/clean_configs_folder"); + TranslateCommand translateCommandForCopyingTemp = new TranslateCommand(); + Path backupFolder = Files.createTempDirectory("temp_back_up_dir"); + translateCommandForCopyingTemp.copyDirectoryContent(cleanConfigsFolder, backupFolder); + TranslateCommand translateCommandSpy = Mockito.spy(translateCommand); + translateCommandSpy.mode = "extract"; + translateCommandSpy.extractionType = "configs"; + translateCommandSpy.resourceFile = cleanConfigsFolder.toString(); + translateCommandSpy.run(); + Mockito.verify(translateCommandSpy, Mockito.atLeast(2)) + .copyDirectoryContent(Mockito.any(), Mockito.any()); + Mockito.verify(translateCommandSpy, Mockito.atLeast(1)) + .deleteDirectoryRecursively(Mockito.any()); + // restore clean_configs_folder + translateCommandForCopyingTemp.copyDirectoryContent(backupFolder, cleanConfigsFolder); + } + + @Test + public void testRunExtractConfigWithDirtyConfigsFolderDeletesTempFileOnFailure() + throws RuntimeException { + Path cleanConfigsFolder = Paths.get("src/test/resources/dirty_configs_folder"); + TranslateCommand translateCommandSpy = Mockito.spy(translateCommand); + translateCommandSpy.mode = "extract"; + translateCommandSpy.extractionType = "configs"; + translateCommandSpy.resourceFile = cleanConfigsFolder.toString(); + + assertThrows(RuntimeException.class, translateCommandSpy::run); + Mockito.verify(translateCommandSpy, Mockito.atLeast(1)) + .copyDirectoryContent(Mockito.any(), Mockito.any()); + Mockito.verify(translateCommandSpy, Mockito.atLeast(1)) + .deleteDirectoryRecursively(Mockito.any()); + } + @Test public void testRunMerge() throws IOException { Path rawQuestionnairePath = Paths.get("src/test/resources/raw_questionnaire.json"); diff --git a/efsity/src/test/resources/clean_configs_folder/profile_config.json b/efsity/src/test/resources/clean_configs_folder/profile_config.json new file mode 100644 index 00000000..c5852900 --- /dev/null +++ b/efsity/src/test/resources/clean_configs_folder/profile_config.json @@ -0,0 +1,61 @@ +{ + "appId": "app_id_name", + "configType": "profile", + "id": "config_id", + "fhirResource": { + "baseResource": { + "resource": "Patient" + }, + "relatedResources": [ + { + "resource": "Condition", + "searchParameter": "subject" + } + ]}, + "rules": [ + { + "name": "patientFirstName", + "condition": "true", + "actions": [ + "data.put('patientFirstName', fhirPath.extractValue(Patient, \"Patient.name[0].select(given)\"))" + ] + }, + { + "name": "patientMiddleName", + "condition": "true", + "actions": [ + "data.put('patientMiddleName', fhirPath.extractValue(Patient, \"Patient.name[0].select(text[0])\"))" + ] + }, + { + "name": "patientLastName", + "condition": "true", + "actions": [ + "data.put('patientLastName', fhirPath.extractValue(Patient, \"Patient.name[0].select(family)\"))" + ] + } + ], + "views": [ + { + "viewType": "COLUMN", + "children": [ + { + "viewType": "CARD", + "padding": 0 + } + ] + } + ], + "overFlowMenuItems": [ + { + "title": "Edit Client Info", + "titleColor": "@{patientTextColor}", + "visible": "true", + "enabled": "@{patientActive}", + "icon": { + "type": "local", + "reference": "ic_user" + } + } + ] +} \ No newline at end of file diff --git a/efsity/src/test/resources/dirty_configs_folder/profile_config.json b/efsity/src/test/resources/dirty_configs_folder/profile_config.json new file mode 100644 index 00000000..51c72c9e --- /dev/null +++ b/efsity/src/test/resources/dirty_configs_folder/profile_config.json @@ -0,0 +1,61 @@ +{ + "appId": "app_id_name", + "configType": "profile", + "id": "config_id", + "fhirResource": { + "baseResource": { + "resource": "Patient" + }, + "relatedResources": [ + { + "resource": "Condition", + "searchParameter": "subject" + } + ]}, + "rules": [ + { + "name": "patientFirstName", + "condition": "true", + "actions": [ + "data.put('patientFirstName', fhirPath.extractValue(Patient, \"Patient.name[0].select(given)\"))" + ] + }, + { + "name": "patientMiddleName", + "condition": "true", + "actions": [ + "data.put('patientMiddleName', fhirPath.extractValue(Patient, \"Patient.name[0].select(text[0])\"))" + ] + }, + { + "name": "patientLastName", + "condition": "true", + "actions": [ + "data.put('patientLastName', fhirPath.extractValue(Patient, \"Patient.name[0].select(family)\"))" + ] + } + ], + "views": [ + { + "viewType": "COLUMN", + "children": [ + { + "viewType": "CARD", + "padding": 0 + } + ] + } + ], + "overFlowMenuItems": [ + { + "title": "Edit Client Info",u + "titleColor": "@{patientTextColor}", + "visible": "true", + "enabled": "@{patientActive}", + "icon": { + "type": "local", + "reference": "ic_user" + } + } + ] +} \ No newline at end of file