diff --git a/README.md b/README.md
index 16689ea6..86a3c918 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,11 @@ A command line utility to support FHIRCore content authoring. This tool supports
Download the latest release from https://github.com/opensrp/fhircore-tooling/releases
-To run it as a java jar use the command `java -jar efsity-1.0.0.jar -h` . This is the help command and will list the available options.
+To run it as a java jar by using the command `java -jar efsity-2.0.0.jar -h` . This is the help command and will list the available options.
If you are using a linux environment e.g. bash you can choose to create an _alias_ for this as shown below. _(Remember to reload the terminal)_
-`alias fct='java -jar ~/Downloads/efsity-1.0.0.jar'`
+`alias fct='java -jar ~/Downloads/efsity-2.0.0.jar'`
To run the previous help command you can then run `fct -h` in your terminal.
@@ -60,7 +60,12 @@ The above will output a list of errors and warnings based on any configuration r
-c or --composition - the composition json file of the project
-i or --input - the input directory path. This should point to the folder with the app configurations e.g. ~/Workspace/fhir-resources/ecbis_cha_preview/
-o or --output - the output path, can be a file or directory. Optional - default is current directory
+-sm or --structure-maps - (Optional) the directory path to the location of structure map .txt or .map files. Must be a directory. Must be used with the -q flag
+-q or --questionnaires - (Optional) the directory path to the location of questionnaires .json files. Must be a directory. Must be used with the -sm flag
```
+
+**Note:** To include _Questionnaire_ and _Structure Map_ validation add the `-sm` and `-q` flags
+
**Sample screenshot output**
diff --git a/pom.xml b/pom.xml
index 711df400..fe018c0f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.smartregister
fhircore-tooling
- 1.0.0
+ 2.0.0
efsity
@@ -118,22 +118,48 @@
org.opencds.cqf.cql
evaluator
- ${opencds.cql.version}
+ ${cql.version}
+
+
+ ca.uhn.hapi.fhir
+ org.hl7.fhir.r5
+
+
+ ca.uhn.hapi.fhir
+ org.hl7.fhir.dstu2
+
+
+ ca.uhn.hapi.fhir
+ org.hl7.fhir.dstu3
+
+
+ ca.uhn.hapi.fhir
+ org.hl7.fhir.dstu2016may
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-structures-dstu3
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-structures-r5
+
+
org.opencds.cqf.cql
evaluator.builder
- ${opencds.cql.version}
+ ${cql.version}
org.opencds.cqf.cql
evaluator.dagger
- ${opencds.cql.version}
+ ${cql.version}
org.opencds.cqf.cql
evaluator.plandefinition
- ${opencds.cql.version}
+ ${cql.version}
@@ -291,19 +317,29 @@
-
org.apache.maven.plugins
- maven-jar-plugin
- ${maven.jar.plugin.version}
-
-
-
- true
- libs/
- org.smartregister.Main
-
-
-
+ maven-assembly-plugin
+
+
+
+ single
+
+ package
+
+
+
+ org.smartregister.Main
+
+
+
+ src/main/resources/dependency-set.xml
+
+
+ jar-with-dependencies
+
+
+
+
diff --git a/src/main/java/org/smartregister/Main.java b/src/main/java/org/smartregister/Main.java
index 091690dd..f5e5ca87 100644
--- a/src/main/java/org/smartregister/Main.java
+++ b/src/main/java/org/smartregister/Main.java
@@ -11,7 +11,7 @@
@Command(
name = "fct",
description = "FHIRCore tooling to make content authoring easier.",
- version = "1.0.0",
+ version = "2.0.0",
mixinStandardHelpOptions = true,
subcommands = {
ConvertCommand.class,
@@ -19,7 +19,7 @@
ValidateCommand.class
})
public class Main implements Runnable {
- public static final String VERSION = "1.0.0";
+ public static final String VERSION = "2.0.0";
@CommandLine.Option(
names = {"-v"},
diff --git a/src/main/java/org/smartregister/command/ValidateCommand.java b/src/main/java/org/smartregister/command/ValidateCommand.java
index 5ea01d0e..36026175 100644
--- a/src/main/java/org/smartregister/command/ValidateCommand.java
+++ b/src/main/java/org/smartregister/command/ValidateCommand.java
@@ -24,17 +24,53 @@ public class ValidateCommand implements Runnable {
required = true)
private String compositionFilePath;
+ @CommandLine.Option(
+ names = {"-sm", "--structure-maps"},
+ description =
+ "directory path to the location of structure map .txt or .map files. Must be a directory. Must be used with the -q flag",
+ required = false)
+ private String structureMapsFolderPath;
+
+ @CommandLine.Option(
+ names = {"-q", "--questionnaires"},
+ description =
+ "directory path to the location of questionnaires .json files. Must be a directory. Must be used with the -sm flag",
+ required = false)
+ private String questionnairesFolderPath;
+
@Override
public void run() {
if (!Files.isDirectory(Paths.get(inputFolder))) {
- throw new RuntimeException("path needs to be a directory");
+ throw new RuntimeException("-i, --input configs input path needs to be a directory");
+ }
+
+ if (structureMapsFolderPath != null && questionnairesFolderPath == null) {
+
+ throw new RuntimeException(
+ "You have supplied a -sm, --structure-maps flag without a corresponding -q, --questionnaires flag");
+
+ } else if (structureMapsFolderPath == null && questionnairesFolderPath != null) {
+ throw new RuntimeException(
+ "You have supplied a -q, --questionnaires flag without a corresponding -sm, --structure-maps flag");
+ }
+
+ if (structureMapsFolderPath != null) {
+
+ if (!Files.isDirectory(Paths.get(structureMapsFolderPath))) {
+ throw new RuntimeException("-sm, --structure-maps path needs to be a directory");
+ }
+
+ if (!Files.isDirectory(Paths.get(questionnairesFolderPath))) {
+ throw new RuntimeException("-q, --questionnaires path needs to be a directory");
+ }
}
try {
FCTValidationEngine FCTValidationEngine = new FCTValidationEngine();
- FCTValidationEngine.process(compositionFilePath, inputFolder);
+ FCTValidationEngine.process(
+ compositionFilePath, structureMapsFolderPath, questionnairesFolderPath, inputFolder);
} catch (IOException e) {
logger.severe(e.getMessage());
diff --git a/src/main/java/org/smartregister/util/FCTUtils.java b/src/main/java/org/smartregister/util/FCTUtils.java
index f062dfbd..34d2723f 100644
--- a/src/main/java/org/smartregister/util/FCTUtils.java
+++ b/src/main/java/org/smartregister/util/FCTUtils.java
@@ -12,9 +12,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Properties;
import org.smartregister.domain.FCTFile;
@@ -123,6 +124,31 @@ public static void printCompletedInDuration(long startTime) {
FCTUtils.getHumanDuration(System.currentTimeMillis() - startTime)));
}
+ public static Map> indexConfigurationFiles(String inputDirectoryPath)
+ throws IOException {
+ Map> filesMap = new HashMap<>();
+ Path rootDir = Paths.get(inputDirectoryPath);
+ Files.walkFileTree(
+ rootDir,
+ new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ if (!Files.isDirectory(file)) {
+
+ String parentDirKey =
+ file.getParent().equals(rootDir)
+ ? FCTValidationEngine.Constants.ROOT
+ : file.getParent().getFileName().toString();
+ Map fileList = filesMap.getOrDefault(parentDirKey, new HashMap<>());
+ fileList.put(file.getFileName().toString(), file.toAbsolutePath().toString());
+ filesMap.put(parentDirKey, fileList);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ return filesMap;
+ }
+
public static final class Constants {
public static final String HL7_FHIR_PACKAGE = "hl7.fhir.r4.core";
public static final String HL7_FHIR_PACKAGE_VERSION = "4.0.1";
diff --git a/src/main/java/org/smartregister/util/FCTValidationEngine.java b/src/main/java/org/smartregister/util/FCTValidationEngine.java
index d415293a..7ee9138c 100644
--- a/src/main/java/org/smartregister/util/FCTValidationEngine.java
+++ b/src/main/java/org/smartregister/util/FCTValidationEngine.java
@@ -3,12 +3,7 @@
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -29,6 +24,7 @@ public class FCTValidationEngine {
private Map>> errorsMap = new HashMap<>();
private JSONObject currentParamDataWorkflowJSONObject;
+ private JSONObject lastWorkflowJSONObject;
private JSONObject previousParentJSONObject;
private JSONObject parentJSONObject;
private String parentJSONObjectKey;
@@ -36,6 +32,9 @@ public class FCTValidationEngine {
private int configurationFilesCount;
private Set factMapKeys = new HashSet<>();
private Map fileConfigTypeIdentifierToFilenameMap = new HashMap<>();
+ private Map> questionnairesToLinkIds;
+ private Map> structureMapToLinkIds;
+ private Map> questionnaireToStructureMapId;
private void handleValue(String key, Object value, boolean isComposition) {
if (value instanceof JSONArray) {
@@ -51,6 +50,9 @@ private void handleValue(String key, Object value, boolean isComposition) {
previousParentJSONObject = parentJSONObject;
parentJSONObject = (JSONObject) value;
+ if (parentJSONObject != null && parentJSONObject.has(Constants.workflow))
+ lastWorkflowJSONObject = parentJSONObject;
+
handleJSONObject((JSONObject) value, isComposition);
} else {
@@ -151,6 +153,61 @@ private void handleValue(String key, Object value, boolean isComposition) {
fileConfigTypeIdentifierToFilenameMap.getOrDefault(configFileIdentifier, null)))
factMapKeys.add(parentJSONObject.getString(Constants.KEY));
}
+
+ //
+ if (questionnairesToLinkIds != null && Constants.PREPOPULATE.equals(value.toString())) {
+
+ // All Pre-populate ids should be in questionnaire
+ String questionnaireId =
+ lastWorkflowJSONObject
+ .getJSONObject(Constants.questionnaire)
+ .getString(Constants.ID);
+
+ String fieldLinkId = parentJSONObject.getString(Constants.linkId);
+
+ if (questionnairesToLinkIds.containsKey(questionnaireId)
+ && !questionnairesToLinkIds.get(questionnaireId).contains(fieldLinkId)) {
+ addToErrorMap(
+ "Prepopulate",
+ String.format(
+ "\u001b[34mPREP\u001b[0m :: link id \u001b[36m%s\u001b[0m missing in Questionnaire with id \u001b[36m%s\u001b[0m",
+ fieldLinkId, questionnaireId));
+ }
+
+ // All Pre-populate ids should be in Structure map
+ String structureMapId =
+ questionnaireToStructureMapId.containsKey(questionnaireId)
+ && questionnaireToStructureMapId.get(questionnaireId).iterator().hasNext()
+ ? questionnaireToStructureMapId.get(questionnaireId).iterator().next()
+ : null;
+
+ if (structureMapId == null && !questionnaireId.contains("{")) {
+ // Executes multiple times (per pre-populate) - consider move to process() function?
+ addToErrorMap(
+ "Questionnaire",
+ String.format(
+ "\u001b[31mSMAP\u001b[0m :: Structure Map missing for Questionnaire with id \u001b[36m%s\u001b[0m",
+ questionnaireId));
+ }
+
+ if (structureMapToLinkIds.containsKey(structureMapId)
+ && !structureMapToLinkIds.get(structureMapId).contains(fieldLinkId)) {
+ addToErrorMap(
+ "Prepopulate",
+ String.format(
+ "\u001b[34mPREP\u001b[0m :: link id \u001b[36m%s\u001b[0m missing in Structure Map with id \u001b[36m%s\u001b[0m",
+ fieldLinkId, structureMapId));
+ } else if (structureMapId != null
+ && !structureMapToLinkIds.containsKey(structureMapId)
+ && !questionnaireId.contains("{")) {
+ // Executes multiple times (per pre-populate) - consider move to process() function?
+ addToErrorMap(
+ "Questionnaire",
+ String.format(
+ "\u001b[31mSMAP\u001b[0m :: Structure Map with id \u001b[36m%s\u001b[0m missing for Questionnaire with id \u001b[36m%s\u001b[0m",
+ structureMapId, questionnaireId));
+ }
+ }
}
}
}
@@ -214,37 +271,63 @@ private void handleJSONArray(String key, JSONArray jsonArray, boolean isComposit
jsonArrayIterator.forEachRemaining(element -> handleValue(key, element, isComposition));
}
- private Map> indexConfigurationFiles(String inputDirectoryPath)
+ public void process(
+ String compositionPath,
+ String structureMapsFolderPath,
+ String questionnairesFolderPath,
+ String directoryPath)
throws IOException {
- Map> filesMap = new HashMap<>();
- Path rootDir = Paths.get(inputDirectoryPath);
- Files.walkFileTree(
- rootDir,
- new SimpleFileVisitor<>() {
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
- if (!Files.isDirectory(file)) {
-
- String parentDirKey =
- file.getParent().equals(rootDir)
- ? FCTValidationEngine.Constants.ROOT
- : file.getParent().getFileName().toString();
- Map fileList = filesMap.getOrDefault(parentDirKey, new HashMap<>());
- fileList.put(file.getFileName().toString(), file.toAbsolutePath().toString());
- filesMap.put(parentDirKey, fileList);
- }
- return FileVisitResult.CONTINUE;
- }
- });
- return filesMap;
- }
-
- public void process(String compositionPath, String directoryPath) throws IOException {
FCTUtils.printToConsole("Processing starting... \uD83D\uDE80");
long startTime = System.currentTimeMillis();
- Map> configDirIndexMap = indexConfigurationFiles(directoryPath);
+ Map> configDirIndexMap =
+ FCTUtils.indexConfigurationFiles(directoryPath);
+ if (questionnairesFolderPath != null) {
+
+ FCTUtils.printInfo("\u001b[36mRunning preprocessor...\u001b[0m");
+
+ Map>> questionnaireProcessorResults =
+ new QuestionnaireProcessor(questionnairesFolderPath).process();
+ questionnairesToLinkIds =
+ questionnaireProcessorResults.getOrDefault(Constants.questionnaire, new HashMap<>());
+ questionnaireToStructureMapId =
+ questionnaireProcessorResults.getOrDefault(Constants.structuremap, new HashMap<>());
+ structureMapToLinkIds = new StructureMapProcessor(structureMapsFolderPath).process();
+
+ FCTUtils.printNewLine();
+ FCTUtils.printInfo("\u001b[36mPRE PARSING VALIDATION\u001b[0m");
+
+ // Validate all Structure Map Link ids should be in questionnaire
+ for (var entry : structureMapToLinkIds.entrySet()) {
+
+ String structureMapId = entry.getKey();
+ String questionnaireID = getQuestionnaireIdByStructureMapId(structureMapId);
+
+ if (questionnaireID != null) {
+
+ Iterator iterator = entry.getValue().iterator();
+ while (iterator.hasNext()) {
+
+ String structureMapLinkId = iterator.next();
+
+ if (!questionnairesToLinkIds.get(questionnaireID).contains(structureMapLinkId)) {
+
+ FCTUtils.printError(
+ String.format(
+ "No Structure Map link id \u001b[36m%s\u001b[0m found in Questionnaire with id \u001b[36m%s\u001b[0m",
+ structureMapLinkId, structureMapId));
+ }
+ }
+
+ } else {
+ FCTUtils.printError(
+ String.format(
+ "No Questionnaire found for Structure Map with id \u001b[36m%s\u001b[0m",
+ structureMapId));
+ }
+ }
+ }
// Load Composition
currentFile = compositionPath;
FCTFile compositionFile = FCTUtils.readFile(compositionPath);
@@ -277,7 +360,8 @@ public void process(String compositionPath, String directoryPath) throws IOExcep
fileJSONObject, Paths.get(compositionPath).equals(Paths.get(nestedEntry.getValue())));
} else {
- FCTUtils.printWarning("Unrecognized Config File Format");
+ FCTUtils.printWarning(
+ String.format("Unrecognized Config File Format for file %s", nestedEntry.getKey()));
}
}
}
@@ -290,6 +374,17 @@ public void process(String compositionPath, String directoryPath) throws IOExcep
FCTUtils.printCompletedInDuration(startTime);
}
+ private String getQuestionnaireIdByStructureMapId(String structureMapId) {
+
+ for (var entry : questionnaireToStructureMapId.entrySet()) {
+ String questionnaireId = entry.getKey();
+
+ if (entry.getValue().contains(structureMapId)) return questionnaireId;
+ }
+
+ return null;
+ }
+
private void resetStatePerFile() {
currentFile = null;
factMapKeys.clear();
@@ -339,7 +434,8 @@ private void printValidationResults() {
StringBuilder errorMessageBuilder =
new StringBuilder(
String.format(
- "%d out of %d files with errors", errorsMap.size(), configurationFilesCount))
+ "%d out of %d configuration files with errors",
+ errorsMap.size(), configurationFilesCount))
.append("\n\n\u001b[32mVALIDATION SUMMARY\u001b[0m \n----------------");
for (var entry : errorsMapCount.entrySet()) {
@@ -373,6 +469,9 @@ public static final class Constants {
public static final String PARAMDATA = "PARAMDATA";
public static final String workflow = "workflow";
public static final String KEY = "key";
+ public static final String linkId = "linkId";
+ public static final String structuremap = "structuremap";
+ public static final String PREPOPULATE = "PREPOPULATE";
public static final Set translatables =
ImmutableSet.of(
"saveButtonText", "title", "display", "actionButtonText", "message"); // , "description"
diff --git a/src/main/java/org/smartregister/util/QuestionnaireProcessor.java b/src/main/java/org/smartregister/util/QuestionnaireProcessor.java
new file mode 100644
index 00000000..fec73a1e
--- /dev/null
+++ b/src/main/java/org/smartregister/util/QuestionnaireProcessor.java
@@ -0,0 +1,145 @@
+/* (C)2023 */
+package org.smartregister.util;
+
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.util.*;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.smartregister.domain.FCTFile;
+
+public class QuestionnaireProcessor {
+ private String directoryPath;
+ private String currentFile;
+ private String currentQuestionnaireId;
+
+ private String currentStructureMapId;
+
+ private Map> questionnairesToLinkIds = new HashMap<>();
+ private Map> questionnairesToStructureMapIds = new HashMap<>();
+
+ private Map>> resultsMap = new HashMap<>();
+
+ public QuestionnaireProcessor(String folderPath) {
+ this.directoryPath = folderPath;
+ }
+
+ public Map>> process() {
+
+ try {
+
+ Map> folderTofilesIndexMap =
+ FCTUtils.indexConfigurationFiles(directoryPath);
+
+ // Process other configurations
+ for (var entry : folderTofilesIndexMap.entrySet()) {
+
+ Map fileIndexMap = folderTofilesIndexMap.get(entry.getKey());
+
+ for (var nestedEntry : fileIndexMap.entrySet()) {
+
+ currentFile = nestedEntry.getValue();
+
+ if (nestedEntry.getKey().startsWith(".")) continue;
+
+ FCTFile file = FCTUtils.readFile(nestedEntry.getValue());
+
+ try {
+
+ JSONObject questionnaireJSONObject = new JSONObject(file.getContent());
+ currentQuestionnaireId =
+ questionnaireJSONObject.getString(FCTValidationEngine.Constants.ID);
+ currentStructureMapId =
+ questionnaireJSONObject.has("extension")
+ ? getStructureMapId(questionnaireJSONObject.getJSONArray("extension"))
+ : null;
+
+ questionnairesToStructureMapIds.put(
+ currentQuestionnaireId,
+ currentStructureMapId != null
+ ? Sets.newHashSet(currentStructureMapId)
+ : Sets.newHashSet());
+ resultsMap.put(
+ FCTValidationEngine.Constants.structuremap, questionnairesToStructureMapIds);
+
+ handleJSONObject(questionnaireJSONObject, true);
+
+ } catch (JSONException jsonException) {
+
+ FCTUtils.printError(String.format("Error processing file %s", currentFile));
+ FCTUtils.printError(String.format("Error message %s", jsonException.getMessage()));
+ }
+ }
+ }
+
+ } catch (IOException ioException) {
+ ioException.toString();
+ }
+
+ resultsMap.put(FCTValidationEngine.Constants.questionnaire, questionnairesToLinkIds);
+ return resultsMap;
+ }
+
+ private String getStructureMapId(JSONArray extensionJSONArray) {
+
+ for (int i = 0; i < extensionJSONArray.length(); i++) {
+
+ if ("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap"
+ .equals(extensionJSONArray.getJSONObject(i).getString("url"))) {
+
+ if (extensionJSONArray.getJSONObject(i).has("valueCanonical")) {
+ return StringUtils.substringAfterLast(
+ extensionJSONArray.getJSONObject(i).optString("valueCanonical").trim(), "/");
+ } else if (extensionJSONArray.getJSONObject(i).has("valueReference")) {
+
+ JSONObject valueReferenceJSONObject =
+ extensionJSONArray.getJSONObject(i).getJSONObject("valueReference");
+ return StringUtils.substringAfterLast(
+ valueReferenceJSONObject.optString("reference").trim(), "/");
+ } else {
+
+ FCTUtils.printError("Structure Map value format not supported");
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void handleValue(String key, Object value, boolean isComposition) {
+ if (value instanceof JSONArray) {
+
+ handleJSONArray(key, (JSONArray) value, isComposition);
+
+ } else if (value instanceof JSONObject) {
+
+ handleJSONObject((JSONObject) value, isComposition);
+
+ } else {
+
+ if (FCTValidationEngine.Constants.linkId.equals(key)) {
+
+ Set results =
+ questionnairesToLinkIds.getOrDefault(currentQuestionnaireId, new HashSet<>());
+ results.add(value.toString());
+ questionnairesToLinkIds.put(currentQuestionnaireId, results);
+ }
+ }
+ }
+
+ private void handleJSONObject(JSONObject jsonObject, boolean isComposition) {
+ Iterator jsonObjectIterator = jsonObject.keys();
+ jsonObjectIterator.forEachRemaining(
+ key -> {
+ Object value = jsonObject.get(key);
+ handleValue(key, value, isComposition);
+ });
+ }
+
+ private void handleJSONArray(String key, JSONArray jsonArray, boolean isComposition) {
+ Iterator