Skip to content

Commit

Permalink
Merge branch 'main' into setup-keycloak-multifactor-authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Lentumunai-Mark authored Jul 29, 2024
2 parents 6b276cd + 313e846 commit 0b5924b
Show file tree
Hide file tree
Showing 49 changed files with 9,485 additions and 536 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A repo to hold our FHIR content and configuration creation tools and scripts.
- [cleaner](https://github.com/onaio/fhircore-tooling/tree/main/cleaner)
- [efsity](https://github.com/onaio/fhircore-tooling/tree/main/efsity)
- [importer](https://github.com/onaio/fhircore-tooling/tree/main/importer)
- [sm-gen](https://github.com/onaio/fhircore-tooling/tree/main/sm-gen)
- [uploader](https://github.com/onaio/fhircore-tooling/tree/main/uploader)

## License
Expand Down
5 changes: 5 additions & 0 deletions efsity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ $ fct publish -e /path/to/env.properties
```
-i or --input : Path to the project folder with the resources to be published
-bu or --fhir-base-url : The base url of the FHIR server to post resources to
-c or --composition-file : The path to the composition file
-at or --access-token : Access token to grant access to the FHIR server
-ci or --client-id : The client identifier for authentication
-cs or --client-secret :The client secret for authentication
Expand All @@ -117,6 +118,10 @@ take precedence over anything in the properties file.
You can either pass the actual accessToken as a variable or pass in the client credentials which will be used
to get an accessToken from the accessToken url provided

You must pass the path to your composition file if you want to publish any binary resources.
The binary resources listed in the composition files are the ones that will be published.
For the publishing of binary resources to work correctly, ensure that you are using the correct/recommended file/folder structure and that the file names in the composition file are in camel case.

### Validating your app configurations
The tool supports some validations for the FHIRCore app configurations. To validate you can run the command:
```console
Expand Down
3 changes: 2 additions & 1 deletion efsity/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repositories {

group = "org.smartregister"

version = "2.3.4-SNAPSHOT"
version = "2.3.8-SNAPSHOT"

description = "fhircore-tooling (efsity)"

Expand Down Expand Up @@ -82,6 +82,7 @@ dependencies {
implementation(deps.jsonschemafriend)
implementation(deps.picocli)
implementation(deps.xstream)
implementation(deps.icu4j)

testImplementation(kotlin("test"))
testImplementation("junit:junit:4.13.2")
Expand Down
2 changes: 2 additions & 0 deletions efsity/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ opencds-cql-version="2.4.0"
project-build-sourceEncoding="UTF-8"
spotless-version ="6.20.0"
xstream="1.4.20"
icu4j-version = "75.1"

[libraries]
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine-version" }
Expand Down Expand Up @@ -44,6 +45,7 @@ jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson-version" }
picocli = { module = "info.picocli:picocli", version.ref = "info-picocli-version" }
xstream = { module = "com.thoughtworks.xstream:xstream", version.ref = "xstream" }
icu4j = { module="com.ibm.icu:icu4j", version.ref = "icu4j-version" }

[bundles]
cqf-cql = ["cql-to-elm","elm","elm-jackson","model","model-jackson"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,29 @@

import static org.smartregister.util.authentication.OAuthAuthentication.getAccessToken;

import com.google.common.annotations.VisibleForTesting;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.TimeZone;
import java.util.UUID;
import java.util.*;
import net.jimblackler.jsonschemafriend.GenerationException;
import net.jimblackler.jsonschemafriend.ValidationException;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.json.JSONArray;
import org.json.JSONObject;
import org.smartregister.domain.FctFile;
import org.smartregister.fhircore_tooling.BuildConfig;
Expand Down Expand Up @@ -97,20 +93,31 @@ public class PublishFhirResourcesCommand implements Runnable {
required = false)
String validateResource = "true";

@CommandLine.Option(
names = {"-c", "--composition"},
description = "path of the composition configuration file",
required = false)
String compositionFilePath;

@Override
public void run() {
long start = System.currentTimeMillis();
if (propertiesFile != null && !propertiesFile.isBlank()) {
try (InputStream inputProperties = new FileInputStream(propertiesFile)) {
Properties properties = new Properties();
properties.load(inputProperties);
setProperties(properties);

Properties properties = null;
try {
properties = FctUtils.readPropertiesFile(propertiesFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
setProperties(properties);
}
try {
publishResources();
if (compositionFilePath != null) {
ArrayList<JSONObject> resourceObjects = buildBinaries(compositionFilePath, projectFolder);
buildBundle(resourceObjects);
}
buildResources();
stateManagement();
} catch (IOException | ValidationException | GenerationException e) {
throw new RuntimeException(e);
Expand All @@ -119,18 +126,21 @@ public void run() {
}

void setProperties(Properties properties) {
if (properties == null)
throw new IllegalStateException("Properties file is missing or could not be parsed");

if (projectFolder == null || projectFolder.isBlank()) {
if (properties.getProperty("projectFolder") != null) {
projectFolder = properties.getProperty("projectFolder");
} else {
throw new NullPointerException("The projectFolder is missing");
throw new IllegalStateException("The projectFolder is missing");
}
}
if (fhirBaseUrl == null || fhirBaseUrl.isBlank()) {
if (properties.getProperty("fhirBaseUrl") != null) {
fhirBaseUrl = properties.getProperty("fhirBaseUrl");
} else {
throw new NullPointerException("The fhirBaseUrl is missing");
throw new IllegalStateException("The fhirBaseUrl is missing");
}
}
if (accessToken == null || accessToken.isBlank()) {
Expand Down Expand Up @@ -170,32 +180,119 @@ void setProperties(Properties properties) {
}
}

void publishResources() throws IOException, ValidationException, GenerationException {
ArrayList<String> resourceFiles = getResourceFiles(projectFolder);
/**
* This function takes in the name of a binary component/resource, converts it from camel case to
* separated by underscores. It then appends a string depending on the name, to return the actual
* file name of the binary file. For example the binaryName 'ancRegister' will be converted to
* 'anc_register_config.json'
*
* @param binaryName This is the name of a binary component/resource as it appears in the
* composition resource. Usually in camel case and matches the start of the actual file name
* @return filename This is the actual file name of the binary resource in the project folder
*/
String getFileName(String binaryName) {
String filename;
if ((binaryName.endsWith("Register")) || (binaryName.endsWith("Profile"))) {
String regex = "([a-z])([A-Z]+)";
String replacer = "$1_$2";
binaryName = binaryName.replaceAll(regex, replacer).toLowerCase();
}
if (binaryName.startsWith("strings")) {
filename = binaryName + "_config.properties";
} else {
filename = binaryName + "_config.json";
}
return filename;
}

HashMap<String, String> getDetails(JSONObject jsonObject) {
JSONObject focus = jsonObject.getJSONObject("focus");
String reference = focus.getString("reference");
JSONObject identifier = focus.getJSONObject("identifier");
String name = identifier.getString("value");

HashMap<String, String> map = new HashMap<>();
map.put("reference", reference);
map.put("name", getFileName(name));
return map;
}

/**
* This function takes in a binary file name and project folder, it then opens the filename in the
* folder ( assuming the recommended folder structure ), reads the content and returns a base64
* encoded version of the content
*
* @param fileName This is the name of the json binary file
* @param projectFolder This is the folder with all the config files
* @return base64 encoded version of the content in the binary json file
* @throws IOException
*/
String getBinaryContent(String fileName, String projectFolder) throws IOException {
String pathToFile;
if (fileName.contains("register")) {
pathToFile = projectFolder + "/registers/" + fileName;
} else if (fileName.contains("profile")) {
pathToFile = projectFolder + "/profiles/" + fileName;
} else if (fileName.startsWith("strings_")) {
pathToFile = projectFolder + "/translations/" + fileName;
} else {
pathToFile = projectFolder + "/" + fileName;
}

String fileContent = FctUtils.readFile(pathToFile).getContent();
return Base64.getEncoder().encodeToString(fileContent.getBytes(StandardCharsets.UTF_8));
}

ArrayList<JSONObject> buildBinaries(String compositionFilePath, String projectFolder)
throws IOException {
FctFile compositionFile = FctUtils.readFile(compositionFilePath);
JSONObject compositionResource = new JSONObject(compositionFile.getContent());
List<Map<String, String>> mapList = new ArrayList<>();
Map<String, String> detailsMap = new HashMap<>();
ArrayList<JSONObject> resourceObjects = new ArrayList<>();
boolean validateResourceBoolean = Boolean.parseBoolean(validateResource);

for (String f : resourceFiles) {
if (validateResourceBoolean) {
FctUtils.printInfo(String.format("Validating file \u001b[35m%s\u001b[0m", f));
ValidateFhirResourcesCommand.validateFhirResources(f);
} else {
FctUtils.printInfo(String.format("Publishing \u001b[35m%s\u001b[0m Without Validation", f));
if (compositionResource.has("section")) {
JSONArray compositionObjects = compositionResource.getJSONArray("section");

for (Object obj : compositionObjects) {
JSONObject jsonObject = new JSONObject(obj.toString());
if (jsonObject.has("section")) {
JSONArray section = jsonObject.getJSONArray("section");
for (Object subObj : section) {
JSONObject jo = new JSONObject(subObj.toString());
detailsMap = getDetails(jo);
}
} else {
detailsMap = getDetails(jsonObject);
}
mapList.add(detailsMap);
}

FctFile inputFile = FctUtils.readFile(f);
JSONObject resourceObject = buildResourceObject(inputFile);
resourceObjects.add(resourceObject);
}
for (Map<String, String> e : mapList) {
String filename = e.get("name");
String binaryContent = getBinaryContent(filename, projectFolder);
String contentType;

// build the bundle
JSONObject bundle = new JSONObject();
bundle.put("resourceType", "Bundle");
bundle.put("type", "transaction");
bundle.put("entry", resourceObjects);
FctUtils.printToConsole("Full Payload to POST: ");
FctUtils.printToConsole(bundle.toString());
if (filename.startsWith("strings_")) {
contentType = "text/plain";
} else {
contentType = "application/json";
}

JSONObject binaryResourceObject = new JSONObject();
binaryResourceObject.put("resourceType", "Binary");
binaryResourceObject.put("id", e.get("reference").substring(7));
binaryResourceObject.put("contentType", contentType);
binaryResourceObject.put("data", binaryContent);

JSONObject finalResourceObject = buildResourceObject(binaryResourceObject.toString());
resourceObjects.add(finalResourceObject);
}
}
return resourceObjects;
}

String getToken() {
if (accessToken == null || accessToken.isBlank()) {
if (clientId == null || clientId.isBlank()) {
throw new IllegalArgumentException(
Expand All @@ -216,7 +313,38 @@ void publishResources() throws IOException, ValidationException, GenerationExcep
accessToken =
getAccessToken(clientId, clientSecret, accessTokenUrl, grantType, username, password);
}
postRequest(bundle.toString(), accessToken);
return accessToken;
}

void buildBundle(ArrayList<JSONObject> resourceObjects) throws IOException {
JSONObject bundle = new JSONObject();
bundle.put("resourceType", "Bundle");
bundle.put("type", "transaction");
bundle.put("entry", resourceObjects);
FctUtils.printToConsole("Full Payload to POST: ");
FctUtils.printToConsole(bundle.toString());

postRequest(bundle.toString(), getToken());
}

void buildResources() throws IOException, ValidationException, GenerationException {
ArrayList<String> resourceFiles = getResourceFiles(projectFolder);
ArrayList<JSONObject> resourceObjects = new ArrayList<>();
boolean validateResourceBoolean = Boolean.parseBoolean(validateResource);

for (String f : resourceFiles) {
if (validateResourceBoolean) {
FctUtils.printInfo(String.format("Validating file \u001b[35m%s\u001b[0m", f));
ValidateFhirResourcesCommand.validateFhirResources(f);
} else {
FctUtils.printInfo(String.format("Publishing \u001b[35m%s\u001b[0m Without Validation", f));
}

FctFile inputFile = FctUtils.readFile(f);
JSONObject resourceObject = buildResourceObject(inputFile.getContent());
resourceObjects.add(resourceObject);
}
buildBundle(resourceObjects);
}

static ArrayList<String> getResourceFiles(String pathToFolder) throws IOException {
Expand Down Expand Up @@ -258,8 +386,8 @@ private static void addFhirResource(String filePath, List<String> filesArray) {
}
}

JSONObject buildResourceObject(FctFile inputFile) {
JSONObject resource = new JSONObject(inputFile.getContent());
JSONObject buildResourceObject(String fileContent) {
JSONObject resource = new JSONObject(fileContent);
String resourceType = null;
String resourceID;
if (resource.has("resourceType")) {
Expand All @@ -275,15 +403,24 @@ JSONObject buildResourceObject(FctFile inputFile) {
request.put("method", "PUT");
request.put("url", resourceType + "/" + resourceID);

ArrayList<JSONObject> tags = new ArrayList<>();
JSONObject version = new JSONObject();
version.put("system", "https://smartregister.org/fct-release-version");
version.put("code", BuildConfig.RELEASE_VERSION);
tags.add(version);

JSONObject meta = new JSONObject();
meta.put("tag", tags);
resource.put("meta", meta);
if (resource.has("meta")) {
JSONObject resource_meta = (JSONObject) resource.get("meta");
if (resource_meta.has("tag")) {
JSONArray resource_tags = resource_meta.getJSONArray("tag");
resource_tags.put(version);
}
} else {
ArrayList<JSONObject> tags = new ArrayList<>();
tags.add(version);

JSONObject meta = new JSONObject();
meta.put("tag", tags);
resource.put("meta", meta);
}

JSONObject object = new JSONObject();
object.put("resource", resource);
Expand Down Expand Up @@ -375,4 +512,9 @@ String getProjectFolder(String projectFolder) {
}
return parentFolder.toString();
}

@VisibleForTesting
public static final String getFCTReleaseVersion() {
return BuildConfig.RELEASE_VERSION;
}
}
Loading

0 comments on commit 0b5924b

Please sign in to comment.