Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDK-344 - SDK should support including spa from a maven artifact #303

Merged
merged 4 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,13 @@ public void assertFilePresent(String... paths) {
assertPathPresent(resolvedPath);
}

public void assertFileContains(String regex, String... paths) throws IOException {
public void assertFileContains(String text, String... paths) throws IOException {
Path resolvedPath = testDirectoryPath.toAbsolutePath();
for (String path : paths) {
resolvedPath = resolvedPath.resolve(path);
}
String jsContents = new String(Files.readAllBytes(resolvedPath), StandardCharsets.UTF_8);
assertThat(jsContents, Matchers.containsString(regex));
assertThat(jsContents, Matchers.containsString(text));
}

public void assertPathPresent(Path path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,24 @@ public void setup_shouldInstallWithParentDistroSpecifiedInDistroProperties() thr
assertFileNotPresent(serverId, "modules", "htmlformentry-3.3.0.omod");
}

@Test
public void setup_shouldInstallWithSpaSpecifiedAsMavenArtifacts() throws Exception{
addTaskParam("distro", testDirectory.toString() + File.separator + "openmrs-distro-spa-artifacts.properties");
addMockDbSettings();

String serverId = UUID.randomUUID().toString();
addAnswer(serverId);
addAnswer("1044");
addAnswer("8080");

executeTask("setup");

assertFilePresent( serverId, "openmrs-2.6.9.war");
assertModulesInstalled(serverId, "spa-2.0.0.omod");
assertFilePresent(serverId, "frontend", "index.html");
assertFileContains("@openmrs/esm-dispensing-app", serverId, "frontend", "importmap.json");
}

private String readValueFromPropertyKey(File propertiesFile, String key) throws Exception {

InputStream in = new FileInputStream(propertiesFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name=Spa Artifact Example
version=1.0.0-SNAPSHOT

war.openmrs=2.6.9

omod.spa=2.0.0

spa.artifactId=openmrs-frontend-zl
spa.groupId=org.pih.openmrs
spa.version=1.3.0
spa.includes=openmrs-frontend-zl-1.3.0

db.h2.supported=false
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.openmrs.maven.plugins.utility;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.MojoExecutionException;
Expand All @@ -17,6 +18,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

import static org.twdata.maven.mojoexecutor.MojoExecutor.Element;
import static org.twdata.maven.mojoexecutor.MojoExecutor.artifactId;
Expand Down Expand Up @@ -56,6 +59,10 @@ public ModuleInstaller(MavenProject mavenProject,
this.versionsHelper = versionsHelper;
}

public ModuleInstaller(DistroHelper distroHelper) {
this(distroHelper.mavenProject, distroHelper.mavenSession, distroHelper.pluginManager, distroHelper.versionHelper);
}

public void installDefaultModules(Server server) throws MojoExecutionException {
boolean isPlatform = server.getVersion() == null; // this might be always true, in which case `getCoreModules` can be simplified
List<Artifact> coreModules = SDKConstants.getCoreModules(server.getPlatformVersion(), isPlatform);
Expand Down Expand Up @@ -92,6 +99,45 @@ public void installModule(Artifact artifact, String outputDir) throws MojoExecut
prepareModules(new Artifact[] { artifact }, outputDir, GOAL_COPY);
}

/**
* @param artifact the artifact retrieve from Maven
* @param outputDir the directory into which the artifact should be unpacked
* @param includes optionally allows limiting the unpacked artifacts to only those specified in the directory that matches this name
* @throws MojoExecutionException
*/
public void installAndUnpackModule(Artifact artifact, File outputDir, String includes) throws MojoExecutionException {
if (!outputDir.exists()) {
throw new MojoExecutionException("Output directory does not exist: " + outputDir);
}
if (StringUtils.isBlank(includes)) {
installAndUnpackModule(artifact, outputDir.getAbsolutePath());
}
else {
File tempDir = null;
try {
tempDir = Files.createTempDirectory(artifact.getArtifactId() + "-" + UUID.randomUUID()).toFile();
installAndUnpackModule(artifact, tempDir.getAbsolutePath());
boolean copied = false;
for (File f : Objects.requireNonNull(tempDir.listFiles())) {
if (f.isDirectory() && f.getName().equals(includes)) {
FileUtils.copyDirectory(f, outputDir);
copied = true;
break;
}
}
if (!copied) {
throw new MojoExecutionException("No directory named " + includes + " exists in artifact " + artifact);
}
}
catch (IOException e) {
throw new MojoExecutionException("Unable to create temporary directory to install " + artifact);
}
finally {
FileUtils.deleteQuietly(tempDir);
}
}
}

public void installAndUnpackModule(Artifact artifact, String outputDir) throws MojoExecutionException {
Path markersDirectory;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.google.common.io.RecursiveDeleteOption;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.openmrs.maven.plugins.model.Artifact;
import org.openmrs.maven.plugins.model.BaseSdkProperties;
import org.openmrs.maven.plugins.model.DistroProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -40,13 +42,20 @@ public class SpaInstaller {
private final DistroHelper distroHelper;

private final ModuleInstaller moduleInstaller;

private final Wizard wizard;

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

public SpaInstaller(DistroHelper distroHelper, NodeHelper nodeHelper) {
this(distroHelper, nodeHelper, new ModuleInstaller(distroHelper), distroHelper.wizard);
}

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

/**
Expand All @@ -65,16 +74,40 @@ public void installFromDistroProperties(File appDataDir, DistroProperties distro
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);
// Three of these properties are not passed to the build tool, but are used to specify the build execution itself
File buildTargetDir = new File(appDataDir, BUILD_TARGET_DIR);

// Retrieve the properties with a spa. prefix out of the distro properties
Map<String, String> spaProperties = distroProperties.getSpaProperties(distroHelper, appDataDir);

// If a maven artifact is defined, then we download the artifact and unpack it
String artifactId = spaProperties.remove(BaseSdkProperties.ARTIFACT_ID);
if (artifactId != null) {
wizard.showMessage("Found spa.artifactId in distro properties: " + artifactId);
String groupId = spaProperties.remove(BaseSdkProperties.GROUP_ID);
String version = spaProperties.remove(BaseSdkProperties.VERSION);
if (groupId == null || version == null) {
throw new MojoExecutionException("If specifying a spa.artifactId, you must also specify a spa.groupId and spa.version property");
}
String type = spaProperties.remove(BaseSdkProperties.ARTIFACT_ID);
String includes = spaProperties.remove(BaseSdkProperties.INCLUDES);
Artifact artifact = new Artifact(artifactId, version, groupId, (type == null ? BaseSdkProperties.TYPE_ZIP : type));
wizard.showMessage("Installing SPA from Maven artifact: " + artifact);
if (buildTargetDir.mkdirs()) {
wizard.showMessage("Created " + BUILD_TARGET_DIR + " directory: " + buildTargetDir.getAbsolutePath());
}
moduleInstaller.installAndUnpackModule(artifact, buildTargetDir, includes);
wizard.showMessage("SPA successfully installed to " + buildTargetDir.getAbsolutePath());
return;
}

// If no maven artifact is defined, then check if npm build configuration is defined

// First pull any optional properties that may be used to specify the core, node, or npm versions
// 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 @@ -83,43 +116,47 @@ public void installFromDistroProperties(File appDataDir, DistroProperties distro
if (npmVersion == null) {
npmVersion = NPM_VERSION;
}

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

File spaConfigFile = new File(appDataDir, "spa-build-config.json");
writeJSONObject(spaConfigFile, spaConfigJson);
// If there are no remaining spa properties, then no spa configuration has been provided
if (spaProperties.isEmpty()) {
wizard.showMessage("No spa configuration found in the distro properties");
return;
}

Properties sdkProperties = getSdkProperties();
boolean reuseNodeCache = (overrideReuseNodeCache != null) ? overrideReuseNodeCache : Boolean.parseBoolean(sdkProperties.getProperty("reuseNodeCache"));
nodeHelper.installNodeAndNpm(nodeVersion, npmVersion, reuseNodeCache);
File buildTargetDir = new File(appDataDir, BUILD_TARGET_DIR);
// If there are remaining spa properties, then build and install using node
Map<String, Object> spaConfigJson = convertPropertiesToJSON(spaProperties);

String program = "openmrs@" + coreVersion;
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 build --target %s --build-config %s", program, buildTargetDir, spaConfigFile), legacyPeerDeps);

Path nodeCache = NodeHelper.tempDir;
if (!reuseNodeCache) {
try {
if (nodeCache != null && nodeCache.toFile().exists()) {
MoreFiles.deleteRecursively(nodeCache, RecursiveDeleteOption.ALLOW_INSECURE);
}
} catch (IOException e) {
logger.error("Couldn't delete the temp file", e);
File spaConfigFile = new File(appDataDir, "spa-build-config.json");
writeJSONObject(spaConfigFile, spaConfigJson);

Properties sdkProperties = getSdkProperties();
boolean reuseNodeCache = (overrideReuseNodeCache != null) ? overrideReuseNodeCache : Boolean.parseBoolean(sdkProperties.getProperty("reuseNodeCache"));
nodeHelper.installNodeAndNpm(nodeVersion, npmVersion, reuseNodeCache);

String program = "openmrs@" + coreVersion;
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 build --target %s --build-config %s", program, buildTargetDir, spaConfigFile), legacyPeerDeps);

Path nodeCache = NodeHelper.tempDir;
if (!reuseNodeCache) {
try {
if (nodeCache != null && nodeCache.toFile().exists()) {
MoreFiles.deleteRecursively(nodeCache, RecursiveDeleteOption.ALLOW_INSECURE);
}
} catch (IOException e) {
logger.error("Couldn't delete the temp file", e);
}
}
}
Expand Down Expand Up @@ -217,5 +254,4 @@ private static void writeJSONObject(File file, Map<String, Object> jsonObject) t
"Exception while writing JSON to \"" + file.getAbsolutePath() + "\" " + e.getMessage(), e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.openmrs.maven.plugins.model.Artifact;
import org.openmrs.maven.plugins.model.BaseSdkProperties;
import org.openmrs.maven.plugins.model.DistroProperties;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

@RunWith(MockitoJUnitRunner.class)
public class SpaInstallerTest {

Expand All @@ -26,16 +36,22 @@ public class SpaInstallerTest {
@Mock
DistroHelper distroHelper;

@Mock
ModuleInstaller moduleInstaller;

@Mock
Wizard wizard;

@Mock
NodeHelper nodeHelper;

File appDataDir;

@Before
public void setup() throws Exception{
public void setup() throws Exception {
appDataDir = new File(ResourceExtractor.simpleExtractResources(getClass(), "/test-tmp"), "server1");
appDataDir.mkdirs();
spaInstaller = new SpaInstaller(distroHelper, nodeHelper);
spaInstaller = new SpaInstaller(distroHelper, nodeHelper, moduleInstaller, wizard);
}

@After
Expand All @@ -62,4 +78,32 @@ public void spaInstall_shouldParseConfigUrlsCorrectly() throws MojoExecutionExce
String spaConfig = FileUtils.readFileToString(spaConfigFile, StandardCharsets.UTF_8);
Assert.assertThat(spaConfig, Matchers.containsString("\"configUrls\":[\"foo\",\"bar\",\"baz\"]"));
}

@Test
public void spaInstall_shouldSupportConfiguringMavenArtifact() throws MojoExecutionException {
Properties distroProperties = new Properties();
distroProperties.setProperty("spa.artifactId", "openmrs-frontend-example");
distroProperties.setProperty("spa.groupId", "org.openmrs.frontend");
distroProperties.setProperty("spa.version", "1.2.3");
spaInstaller.installFromDistroProperties(appDataDir, new DistroProperties(distroProperties));

String expectedOutputDir = new File(appDataDir, "frontend").getAbsolutePath();

ArgumentCaptor<String> wizardMessageCaptor = ArgumentCaptor.forClass(String.class);
verify(wizard, atLeast(3)).showMessage(wizardMessageCaptor.capture());

ArgumentCaptor<Artifact> artifactCaptor = ArgumentCaptor.forClass(Artifact.class);
ArgumentCaptor<File> targetDirectoryCaptor = ArgumentCaptor.forClass(File.class);
ArgumentCaptor<String> includesCaptor = ArgumentCaptor.forClass(String.class);
verify(moduleInstaller, times(1)).installAndUnpackModule(artifactCaptor.capture(), targetDirectoryCaptor.capture(), includesCaptor.capture());
Artifact artifact = artifactCaptor.getValue();
assertThat(artifact.getArtifactId(), equalTo("openmrs-frontend-example"));
assertThat(artifact.getGroupId(), equalTo("org.openmrs.frontend"));
assertThat(artifact.getVersion(), equalTo("1.2.3"));
assertThat(artifact.getType(), equalTo(BaseSdkProperties.TYPE_ZIP));
assertThat(targetDirectoryCaptor.getValue().getAbsolutePath(), equalTo(expectedOutputDir));

// Validate that the build and install from node process did not run
verifyNoInteractions(nodeHelper);
}
}
Loading
Loading