Skip to content

Commit

Permalink
O3-3639: SDK to rebuild frontend with multiple config files (#295)
Browse files Browse the repository at this point in the history
Co-authored-by: Ian <ian.c.bacher@gmail.com>
  • Loading branch information
nravilla and ibacher authored Sep 27, 2024
1 parent 9782465 commit a75f1a2
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.openmrs.maven.plugins.model.DistroProperties;
import org.openmrs.maven.plugins.model.Server;
import org.openmrs.maven.plugins.model.Version;
import org.openmrs.maven.plugins.utility.ContentHelper;
import org.openmrs.maven.plugins.utility.DistroHelper;
import org.openmrs.maven.plugins.model.Project;
import org.openmrs.maven.plugins.utility.SDKConstants;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
Expand All @@ -13,63 +14,99 @@
import org.openmrs.maven.plugins.model.DistroProperties;

/**
* This class is downloads and moves content backend config to respective configuration folders
* This class downloads and moves content backend config to respective configuration folders.
*/
public class ContentHelper {

public static final String FRONTEND_CONFIG_FOLDER = Paths.get("configs", "frontend_config").toString();
public static final String BACKEND_CONFIG_FOLDER = Paths.get("configs", "backend_config").toString();

public static void downloadContent(Artifact contentArtifact, ModuleInstaller moduleInstaller, File targetDir) throws MojoExecutionException {
String artifactId = contentArtifact.getArtifactId();
// create a temporary artifact folder
File sourceDir;
try {
sourceDir = Files.createTempDirectory("openmrs-sdk-" + artifactId + "-").toFile();
} catch (IOException e) {
throw new MojoExecutionException("Exception while trying to create temporary directory", e);
}

moduleInstaller.installAndUnpackModule(contentArtifact, sourceDir.getAbsolutePath());
moveBackendConfig(artifactId, sourceDir, targetDir);
public static final String FRONTEND_CONFIG_FOLDER = Paths.get("configs", "frontend_config").toString();
public static final String BACKEND_CONFIG_FOLDER = Paths.get("configs", "backend_config").toString();

FileUtils.deleteQuietly(sourceDir);
}

private static void moveBackendConfig(String artifactId, File sourceDir, File targetDir) throws MojoExecutionException {
try {
File backendConfigFiles = sourceDir.toPath().resolve(BACKEND_CONFIG_FOLDER).toFile();
Path targetPath = targetDir.toPath().toAbsolutePath();
private static File unpackArtifact(Artifact contentArtifact, ModuleInstaller moduleInstaller) throws MojoExecutionException {
String artifactId = contentArtifact.getArtifactId();
// create a temporary artifact folder
File sourceDir;
try {
sourceDir = Files.createTempDirectory("openmrs-sdk-" + artifactId).toFile();
} catch (IOException e) {
throw new MojoExecutionException("Exception while trying to create temporary directory", e);
}

if (backendConfigFiles.exists()) {
File[] configDirectories = backendConfigFiles.listFiles(File::isDirectory);
if (configDirectories != null) {
for (File config : configDirectories) {
Path destDir = targetPath.resolve(config.getName()).resolve(artifactId);
Files.createDirectories(destDir);
Runtime.getRuntime().addShutdownHook(new Thread(() -> FileUtils.deleteQuietly(sourceDir)));

//copy config files to the matching configuration folder
FileUtils.copyDirectory(config, destDir.toFile());
}
}
}
}
catch (IOException e) {
throw new MojoExecutionException("Error copying backend configuration: " + e.getMessage(), e);
}
}

public static void downloadAndMoveContentBackendConfig(File serverDirectory, DistroProperties distroProperties, ModuleInstaller moduleInstaller, Wizard wizard) throws MojoExecutionException {
if (distroProperties != null) {
File targetDir = new File(serverDirectory, SDKConstants.OPENMRS_SERVER_CONFIGURATION);
List<Artifact> contents = distroProperties.getContentArtifacts();

if (contents != null) {
for (Artifact content : contents) {
wizard.showMessage("Downloading Content: " + content + "\n");
ContentHelper.downloadContent(content, moduleInstaller, targetDir);
}
}
}
}
moduleInstaller.installAndUnpackModule(contentArtifact, sourceDir.getAbsolutePath());
return sourceDir;
}

private static void moveBackendConfig(Artifact contentArtifact, File targetDir, ModuleInstaller moduleInstaller) throws MojoExecutionException {
File sourceDir = unpackArtifact(contentArtifact, moduleInstaller);
try {
File backendConfigFiles = sourceDir.toPath().resolve(BACKEND_CONFIG_FOLDER).toFile();
Path targetPath = targetDir.toPath().toAbsolutePath();

if (backendConfigFiles.exists()) {
File[] configDirectories = backendConfigFiles.listFiles(File::isDirectory);
if (configDirectories != null) {
for (File config : configDirectories) {
Path destDir = targetPath.resolve(config.getName()).resolve(contentArtifact.getArtifactId());
Files.createDirectories(destDir);

// Copy config files to the matching configuration folder
FileUtils.copyDirectory(config, destDir.toFile());
}
}
}
} catch (IOException e) {
throw new MojoExecutionException("Error copying backend configuration: " + e.getMessage(), e);
} finally {
FileUtils.deleteQuietly(sourceDir);
}
}

public static List<File> extractAndGetAllContentFrontendConfigs(Artifact contentArtifact, ModuleInstaller moduleInstaller) throws MojoExecutionException {
File sourceDir = unpackArtifact(contentArtifact, moduleInstaller);
List<File> configFiles = new ArrayList<>();
File frontendConfigFiles = sourceDir.toPath().resolve(FRONTEND_CONFIG_FOLDER).toFile();

if (frontendConfigFiles.exists() && frontendConfigFiles.isDirectory()) {
File[] files = frontendConfigFiles.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.length() > 5) {
configFiles.add(file);
}
}
}
} else {
throw new MojoExecutionException("Error: Frontend configuration folder not found.");
}
return configFiles;
}

// This method now sets the static moduleInstaller
public static void downloadAndMoveContentBackendConfig(File serverDirectory, DistroProperties distroProperties, ModuleInstaller moduleInstaller, Wizard wizard) throws MojoExecutionException {
if (distroProperties != null) {
File targetDir = new File(serverDirectory, SDKConstants.OPENMRS_SERVER_CONFIGURATION);
List<Artifact> contents = distroProperties.getContentArtifacts();

if (contents != null) {
for (Artifact content : contents) {
wizard.showMessage("Downloading Content: " + content + "\n");
moveBackendConfig(content, targetDir, moduleInstaller);
}
}
}
}

public static List<File> collectFrontendConfigs(DistroProperties distroProperties, ModuleInstaller moduleInstaller) throws MojoExecutionException {
List<File> allConfigFiles = new ArrayList<>();
if (distroProperties != null) {
List<Artifact> contents = distroProperties.getContentArtifacts();
if (contents != null) {
for (Artifact contentArtifact : contents) {
allConfigFiles.addAll(extractAndGetAllContentFrontendConfigs(contentArtifact, moduleInstaller));
}
}
}
return allConfigFiles;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
Expand All @@ -23,56 +24,57 @@

public class SpaInstaller {

static final String BAD_SPA_PROPERTIES_MESSAGE = "Distro properties file contains invalid 'spa.' elements. " +
"Please check the distro properties file and the specification. " +
"Parent properties cannot have their own values (i.e., if 'spa.foo.bar' exists, 'spa.foo' cannot be assigned a value). "
+
"Duplicate properties are not allowed.";
static final String BAD_SPA_PROPERTIES_MESSAGE = "Distro properties file contains invalid 'spa.' elements. "
+ "Please check the distro properties file and the specification. "
+ "Parent properties cannot have their own values (i.e., if 'spa.foo.bar' exists, 'spa.foo' cannot be assigned a value). "
+ "Duplicate properties are not allowed.";

static final String NODE_VERSION = "16.15.0";
static final String NODE_VERSION = "20.17.0";

static final String NPM_VERSION = "8.5.5";
static final String NPM_VERSION = "10.8.2";

static final String BUILD_TARGET_DIR = "frontend";
static final String BUILD_TARGET_DIR = "frontend";

private final NodeHelper nodeHelper;

private final DistroHelper distroHelper;

private final ModuleInstaller moduleInstaller;

private static final Logger logger = LoggerFactory.getLogger(SpaInstaller.class);

public SpaInstaller(DistroHelper distroHelper,
NodeHelper nodeHelper) {
public SpaInstaller(DistroHelper distroHelper, NodeHelper nodeHelper) {
this.distroHelper = distroHelper;
this.moduleInstaller = new ModuleInstaller(distroHelper.mavenProject, distroHelper.mavenSession, distroHelper.pluginManager, distroHelper.versionHelper);
this.nodeHelper = nodeHelper;
}

/**
* Installs the SPA Microfrontend application based on entries in the distro properties.
*
* @param appDataDir The application data directory
* @param appDataDir The application data directory
* @param distroProperties Non-null
* @throws MojoExecutionException
*/
public void installFromDistroProperties(File appDataDir, DistroProperties distroProperties)
throws MojoExecutionException {

throws MojoExecutionException {
installFromDistroProperties(appDataDir, distroProperties, false, null);
}

public void installFromDistroProperties(File appDataDir, DistroProperties distroProperties, boolean ignorePeerDependencies, Boolean overrideReuseNodeCache)
throws MojoExecutionException {

// We find all the lines in distro properties beginning with `spa` and convert these
// into a JSON structure. This is passed to the frontend build tool.
// If no SPA elements are present in the distro properties, the SPA is not installed.
Map<String, String> spaProperties = distroProperties.getSpaProperties(distroHelper, appDataDir);

Map<String, String> spaProperties = distroProperties.getSpaProperties(distroHelper, appDataDir);
// Three of these properties are not passed to the build tool, but are used to specify the build execution itself
String coreVersion = spaProperties.remove("core");
if (coreVersion == null) {
coreVersion = "next";
}

String nodeVersion = spaProperties.remove("node");
if (nodeVersion == null) {
nodeVersion = NODE_VERSION;
Expand All @@ -81,7 +83,7 @@ public void installFromDistroProperties(File appDataDir, DistroProperties distro
if (npmVersion == null) {
npmVersion = NPM_VERSION;
}

if (!spaProperties.isEmpty()) {
Map<String, Object> spaConfigJson = convertPropertiesToJSON(spaProperties);

Expand All @@ -97,10 +99,17 @@ public void installFromDistroProperties(File appDataDir, DistroProperties distro
String legacyPeerDeps = ignorePeerDependencies ? "--legacy-peer-deps" : "";
// print frontend tool version number
nodeHelper.runNpx(String.format("%s --version", program), legacyPeerDeps);

if (distroProperties.getContentArtifacts().isEmpty()) {
nodeHelper.runNpx(String.format("%s assemble --target %s --mode config --config %s", program, buildTargetDir,
spaConfigFile), legacyPeerDeps);
} else {
List<File> configFiles = ContentHelper.collectFrontendConfigs(distroProperties, moduleInstaller);
String assembleCommand = assembleWithFrontendConfig(program, buildTargetDir, configFiles, spaConfigFile);
nodeHelper.runNpx(assembleCommand, legacyPeerDeps);
}
nodeHelper.runNpx(
String.format("%s assemble --target %s --mode config --config %s", program, buildTargetDir, spaConfigFile), legacyPeerDeps);
nodeHelper.runNpx(
String.format("%s build --target %s --build-config %s", program, buildTargetDir, spaConfigFile), legacyPeerDeps);
String.format("%s build --target %s --build-config %s", program, buildTargetDir, spaConfigFile), legacyPeerDeps);

Path nodeCache = NodeHelper.tempDir;
if (!reuseNodeCache) {
Expand All @@ -115,6 +124,23 @@ public void installFromDistroProperties(File appDataDir, DistroProperties distro
}
}

private String assembleWithFrontendConfig(String program, File buildTargetDir, List<File> configFiles, File spaConfigFile) {
StringBuilder command = new StringBuilder();
command.append(program)
.append(" assemble --target '")
.append(buildTargetDir)
.append("' --mode config --config '")
.append(spaConfigFile)
.append("'");

for (File configFile : configFiles) {
command.append(" --config-file '").append(configFile.getAbsolutePath()).append("'");
}

return command.toString();
}


private Map<String, Object> convertPropertiesToJSON(Map<String, String> properties) throws MojoExecutionException {
Set<String> foundPropertySetKeys = new HashSet<>();
Map<String, Object> result = new LinkedHashMap<>();
Expand All @@ -128,9 +154,9 @@ private Map<String, Object> convertPropertiesToJSON(Map<String, String> properti
for (String dotDelimitedKeys : properties.keySet()) {
if (foundPropertySetKeys.contains(dotDelimitedKeys)) {
String badLine = "spa." + dotDelimitedKeys + "=" + properties.get(dotDelimitedKeys);
throw new MojoExecutionException(BAD_SPA_PROPERTIES_MESSAGE +
" The following property is a parent property to another, and therefore cannot be assigned a value:\t\""
+ badLine + "\"");
throw new MojoExecutionException(BAD_SPA_PROPERTIES_MESSAGE
+ " The following property is a parent property to another, and therefore cannot be assigned a value:\t\""
+ badLine + "\"");
}
addPropertyToJSONObject(result, dotDelimitedKeys, properties.get(dotDelimitedKeys));
}
Expand All @@ -140,22 +166,22 @@ private Map<String, Object> convertPropertiesToJSON(Map<String, String> properti

/**
* Add a line from the properties file to the new JSON object. Creates nested objects as needed.
* Known array-valued keys are parsed as comma-delimited arrays; e.g.,
* `configUrls=qux` becomes `{ "configUrls": ["qux"] }` because `configUrls` is known to be array-valued
* Known array-valued keys are parsed as comma-delimited arrays; e.g., `configUrls=qux` becomes
* `{ "configUrls": ["qux"] }` because `configUrls` is known to be array-valued
*
* @param jsonObject the object being constructed
* @param jsonObject the object being constructed
* @param propertyKey
* @param value
*/
private void addPropertyToJSONObject(Map<String, Object> jsonObject, String propertyKey, String value)
throws MojoExecutionException {
throws MojoExecutionException {
String[] keys = propertyKey.split("\\.");

if (keys.length == 1) {
if (jsonObject.containsKey(keys[0])) {
throw new MojoExecutionException(BAD_SPA_PROPERTIES_MESSAGE +
" Encountered this error processing a property containing the key '" + keys[0] + "' and with value "
+ value);
throw new MojoExecutionException(
BAD_SPA_PROPERTIES_MESSAGE + " Encountered this error processing a property containing the key '"
+ keys[0] + "' and with value " + value);
}

if ("configUrls".equals(keys[0])) {
Expand All @@ -171,8 +197,8 @@ private void addPropertyToJSONObject(Map<String, Object> jsonObject, String prop

Object childObject = jsonObject.get(keys[0]);
if (!(childObject instanceof Map)) {
throw new MojoExecutionException(BAD_SPA_PROPERTIES_MESSAGE +
" Also please post to OpenMRS Talk and include this full message. If you are seeing this, there has been a programming error.");
throw new MojoExecutionException(BAD_SPA_PROPERTIES_MESSAGE
+ " Also please post to OpenMRS Talk and include this full message. If you are seeing this, there has been a programming error.");
}

@SuppressWarnings("unchecked")
Expand All @@ -188,8 +214,8 @@ private static void writeJSONObject(File file, Map<String, Object> jsonObject) t
om.writeValue(file, jsonObject);
}
catch (IOException e) {
throw new MojoExecutionException("Exception while writing JSON to \"" + file.getAbsolutePath() + "\" "
+ e.getMessage(), e);
throw new MojoExecutionException(
"Exception while writing JSON to \"" + file.getAbsolutePath() + "\" " + e.getMessage(), e);
}
}

Expand Down

0 comments on commit a75f1a2

Please sign in to comment.